Cách điều hướng qua các trường văn bản (Nút Tiếp theo / Xong)


487

Làm cách nào tôi có thể điều hướng qua tất cả các trường văn bản của mình bằng nút "Tiếp theo" trên Bàn phím iPhone?

Các lĩnh vực văn bản cuối cùng sẽ đóng bàn phím.

Tôi đã thiết lập IB các Nút (Tiếp theo / Xong) nhưng bây giờ tôi bị kẹt.

Tôi đã triển khai hành động textFieldShouldReturn nhưng bây giờ các nút Tiếp theo và Xong đóng Bàn phím.


Xin hãy xem câu trả lời của tôi dưới đây. Theo kinh nghiệm của tôi, đó là một giải pháp tốt hơn, đầy đủ hơn so với câu trả lời thường được đưa ra. Tôi không biết tại sao mọi người sẽ đánh giá tiêu cực.
memmons

Tôi có vấn đề tương tự nhưng với Cordova, Phonegap Có cách nào để giải quyết nó không?

Câu trả lời:


578

Trong Ca cao cho Mac OS X, bạn có chuỗi phản hồi tiếp theo, nơi bạn có thể hỏi trường văn bản điều khiển nào cần tập trung tiếp theo. Đây là những gì làm cho tab giữa các trường văn bản hoạt động. Nhưng vì các thiết bị iOS không có bàn phím, chỉ có cảm ứng, khái niệm này đã không tồn tại trong quá trình chuyển đổi sang Cốc Cốc.

Điều này có thể dễ dàng thực hiện bằng mọi cách, với hai giả định:

  1. Tất cả các "tabbable" UITextFieldđều nằm trên cùng một khung nhìn cha.
  2. "Thứ tự tab" của chúng được xác định bởi thuộc tính thẻ.

Giả sử điều này bạn có thể ghi đè văn bảnFieldShouldReturn: như thế này:

-(BOOL)textFieldShouldReturn:(UITextField*)textField
{
  NSInteger nextTag = textField.tag + 1;
  // Try to find next responder
  UIResponder* nextResponder = [textField.superview viewWithTag:nextTag];
  if (nextResponder) {
    // Found next responder, so set it.
    [nextResponder becomeFirstResponder];
  } else {
    // Not found, so remove keyboard.
    [textField resignFirstResponder];
  }
  return NO; // We do not want UITextField to insert line-breaks.
}

Thêm một số mã, và các giả định cũng có thể được bỏ qua.

Swift 4.0

 func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    let nextTag = textField.tag + 1
    // Try to find next responder
    let nextResponder = textField.superview?.viewWithTag(nextTag) as UIResponder!

    if nextResponder != nil {
        // Found next responder, so set it
        nextResponder?.becomeFirstResponder()
    } else {
        // Not found, so remove keyboard
        textField.resignFirstResponder()
    }

    return false
}

Nếu giám sát của trường văn bản sẽ là UITableViewCell thì phần trả lời tiếp theo sẽ là

let nextResponder = textField.superview?.superview?.superview?.viewWithTag(nextTag) as UIResponder!

6
Đôi khi, có các nút "tiếp theo" và "trước" trên đầu bàn phím. Ít nhất là trong trình duyệt.
Tim Büthe

31
Chỉ để được mô phạm, tôi muốn làm rõ một số thông tin sai trong bài viết này. Trên OS X, thứ tự tab được thiết lập thông qua phương thức setNextKeyView của NSView: (không phải setNextResponder :). Chuỗi phản hồi tách biệt với điều này: đó là một hệ thống phân cấp các đối tượng phản hồi xử lý các hành động "không nhắm mục tiêu", ví dụ: các hành động được gửi đến phản hồi đầu tiên thay vì trực tiếp đến đối tượng điều khiển. Chuỗi trả lời thường tuân theo phân cấp chế độ xem, cho đến cửa sổ và bộ điều khiển của nó và sau đó là đại biểu ứng dụng. Google "chuỗi phản hồi" cho nhiều thông tin.
davehayden

Giám sát của trường văn bản sẽ là a UITableViewCell. Bạn sẽ cần truy cập vào liên kết UITableViewvà lấy ô chứa trường văn bản bạn đang tìm kiếm.
Michael Mior

13
Đừng quên thêm UITextFieldDelegatevà thiết lập các đại biểu trường văn bản.
Sal

1
Lời nhắc thân thiện để đặt tùy chọn TAG từ bảng phân cảnh (trong trường hợp của tôi). Bạn phải đặt thủ công thẻ TextFields theo thứ tự tăng dần. (Đầu tiên là 0, thứ hai là 1, v.v.) để giải pháp này hoạt động.
Joakim

170

Có một nhiều giải pháp thanh lịch hơn mà thổi tôi đi lần đầu tiên tôi nhìn thấy nó. Những lợi ích:

  • Gần hơn với việc triển khai trường văn bản OSX trong đó trường văn bản biết nơi tập trung sẽ đi tới
  • Không dựa vào cài đặt hoặc sử dụng thẻ - IMO mong manh cho trường hợp sử dụng này
  • Có thể được mở rộng để hoạt động với cả hai UITextFieldUITextViewđiều khiển - hoặc bất kỳ điều khiển UI nhập bàn phím nào
  • Không làm lộn xộn bộ điều khiển khung nhìn của bạn với mã ủy nhiệm UITextField soạn sẵn
  • Tích hợp độc đáo với IB và có thể được cấu hình thông qua tùy chọn kéo thả tùy chọn quen thuộc để kết nối các cửa hàng.

Tạo một lớp con UITextField có thuộc IBOutlettính được gọi là nextField. Đây là tiêu đề:

@interface SOTextField : UITextField

@property (weak, nonatomic) IBOutlet UITextField *nextField; 

@end

Và đây là việc thực hiện:

@implementation SOTextField

@end

Trong trình điều khiển chế độ xem của bạn, bạn sẽ tạo -textFieldShouldReturn:phương thức ủy nhiệm:

- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    if ([textField isKindOfClass:[SOTextField class]]) {
        UITextField *nextField = [(SOTextField *)textField nextField];

        if (nextField) {
            dispatch_async(dispatch_get_current_queue(), ^{
                [nextField becomeFirstResponder];
            });
        }
        else {
            [textField resignFirstResponder];
        }
    }

    return YES;
}

Trong IB, thay đổi UITextFields của bạn để sử dụng SOTextFieldlớp. Tiếp theo, cũng trong IB, đặt đại biểu cho từng 'SOTextFields'to' Chủ sở hữu tệp '(đúng nơi bạn đặt mã cho phương thức ủy nhiệm - textFieldShouldReturn). Cái hay của thiết kế này là bây giờ bạn có thể chỉ cần nhấp chuột phải vào bất kỳ textField nào và gán ổ cắm nextField cho SOTextFieldđối tượng tiếp theo mà bạn muốn làm phản hồi tiếp theo.

Chỉ định NextField trong IB

Hơn nữa, bạn có thể thực hiện những điều thú vị như lặp lại textFields để sau lần cuối cùng mất tiêu điểm, lần đầu tiên sẽ nhận được tiêu điểm một lần nữa.

Điều này có thể dễ dàng được mở rộng để tự động gán returnKeyTypecủa SOTextFieldmột UIReturnKeyNextnếu có một nextField giao - một ít điều bằng tay cấu hình.


3
Tôi thực sự thích tích hợp IB hơn là sử dụng các thẻ - đặc biệt là với các phiên bản Xcode mới hơn tích hợp IB độc đáo với phần còn lại của IDE - và không phải làm lộn xộn bộ điều khiển của tôi với mã ủy nhiệm soạn thảo văn bản sôi nổi là một phần thưởng.
memmons

4
Thẻ dễ dàng hơn, nhưng cách này tốt hơn và phù hợp hơn với thiết kế OO tốt
Brain2000

1
Tôi thực sự thích giải pháp này, nhưng tôi đang sử dụng từng trường văn bản trong các chế độ xem ô riêng biệt trong a UITableView. Mặc dù tôi có IBOutletcác thuộc tính trên trình điều khiển xem cho từng trường văn bản, nhưng nó không cho phép tôi liên kết thuộc nextFieldtính với các thuộc tính đó trên Chủ sở hữu tệp. Bất kỳ lời khuyên làm thế nào tôi có thể làm cho nó hoạt động trong kịch bản này?
Jay Imerman

1
@JayImerman IB sẽ không cho phép bạn kết nối các cửa hàng giữa các ô. Vì vậy, IB sẽ là không có trong kịch bản này. Tuy nhiên, bạn chỉ có thể kết nối các thuộc tính nextField bằng mã. textField1.nextField = textField2; textField2.nextField = textField3;, v.v.
memmons

2
Bất kỳ cơ hội nào câu trả lời này có thể được cập nhật cho các phiên bản mới hơn của xcode và ios?
lostintranslation

82

Đây là một không có phái đoàn:

tf1.addTarget(tf2, action: #selector(becomeFirstResponder), for: .editingDidEndOnExit)
tf2.addTarget(tf3, action: #selector(becomeFirstResponder), for: .editingDidEndOnExit)

ObjC:

[tf1 addTarget:tf2 action:@selector(becomeFirstResponder) forControlEvents:UIControlEventEditingDidEndOnExit];
[tf2 addTarget:tf3 action:@selector(becomeFirstResponder) forControlEvents:UIControlEventEditingDidEndOnExit];

Hoạt động bằng cách sử dụng UIControlEventEditingDidEndOnExit UITextFieldhành động (hầu hết không biết) .

Bạn cũng có thể dễ dàng kết nối điều này trong bảng phân cảnh, do đó không yêu cầu ủy quyền hoặc mã.

Chỉnh sửa: thực sự tôi không thể tìm ra làm thế nào để kết nối điều này trong bảng phân cảnh. becomeFirstResponderdường như không phải là một hành động được cung cấp cho sự kiện kiểm soát này, đó là một điều đáng tiếc. Tuy nhiên, bạn có thể kết nối tất cả các trường văn bản của mình với một hành động duy nhất trong ViewContoder để xác định textField nào becomeFirstResponderdựa trên người gửi (mặc dù vậy nó không thanh lịch như giải pháp lập trình ở trên để IMO thực hiện với mã ở trên viewDidLoad).


18
Giải pháp rất đơn giản và hiệu quả. Cái cuối cùng trong chuỗi thậm chí có thể kích hoạt, ví dụ [tfN addTarget:self action:@selector(loginButtonTapped:) forControlEvents:UIControlEventEditingDidEndOnExit];. Cảm ơn @mxcl.
msrdjan

Làm thế nào bạn sẽ làm điều này trong bảng phân cảnh?
Pedro Borges

Đây có lẽ là câu trả lời tốt nhất ở đây.
nghệ sĩ daspian

Điều này làm việc cho tôi hoàn hảo ngay cả trong Swift 2. Đây là một giải pháp đơn giản và nhanh chóng hơn nhiều so với việc thêm vào các đại biểu.
tấn

2
Swift 4: txtFirstname.addTarget (txtLastname, hành động: #selector (trở thành FirstResponder), cho: UIControlEvents.editingDidEndOnExit)
realtimez

78

Đây là giải pháp của tôi cho vấn đề này.

Để giải quyết vấn đề này (và vì tôi ghét dựa vào các thẻ để làm công cụ), tôi quyết định thêm một thuộc tính tùy chỉnh vào đối tượng UITextField. Nói cách khác, tôi đã tạo một danh mục trên UITextField như thế này:

UITextField + Extended.h

@interface UITextField (Extended)

@property(retain, nonatomic)UITextField* nextTextField;

@end

UITextField + Extended.m

#import "UITextField+Extended.h"
#import <objc/runtime.h>

static char defaultHashKey;

@implementation UITextField (Extended)

- (UITextField*) nextTextField { 
    return objc_getAssociatedObject(self, &defaultHashKey); 
}

- (void) setNextTextField:(UITextField *)nextTextField{
    objc_setAssociatedObject(self, &defaultHashKey, nextTextField, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
}

@end

Bây giờ, đây là cách tôi sử dụng nó:

UITextField *textField1 = ...init your textfield
UITextField *textField2 = ...init your textfield
UITextField *textField3 = ...init your textfield

textField1.nextTextField = textField2;
textField2.nextTextField = textField3;
textField3.nextTextField = nil;

Và thực hiện phương thức textFieldShouldReturn:

- (BOOL)textFieldShouldReturn:(UITextField *)theTextField {

    UITextField *next = theTextField.nextTextField;
    if (next) {
        [next becomeFirstResponder];
    } else {
        [theTextField resignFirstResponder];
    }

    return NO; 
}

Bây giờ tôi có một danh sách liên kết của UITextField, mỗi người biết ai là người tiếp theo trong dòng.

Hy vọng nó sẽ giúp.


11
Đây là giải pháp tốt nhất và nên được chọn là câu trả lời.
Giovanni

4
Giải pháp này hiệu quả với tôi, điều duy nhất tôi đã thay đổi là tạo nextTextField và IBOutlet để tôi có thể thiết lập chuỗi từ trong Storyboard của mình.
zachzurn

Nếu tôi có thể lưu câu trả lời yêu thích, đây sẽ là một trong số đó. Cảm ơn các giải pháp sạch!
Matt Becker

1
Lưu ý rằng nếu IB là thứ của bạn, bạn cũng có thể đặt các thuộc tính này là IBOutlets. . cẩn thận với va chạm tên - Apple cuối cùng có thể thêm điều này vào UIResponder.
Jasper Blues

Giải pháp này có tương thích với trình xây dựng giao diện không? Ý tôi là nó có thể sử dụng IBOutlets và tham chiếu bảng phân cảnh thay vì đặt nextTextField theo chương trình không? IB dường như không cho phép bạn đặt lớp UITextField thành một danh mục.
Pedro Borges

46

Một tiện ích mở rộng nhanh chóng áp dụng câu trả lời của mxcl để thực hiện việc này đặc biệt dễ dàng (được điều chỉnh theo swift 2.3 bởi Traveller):

extension UITextField {
    class func connectFields(fields:[UITextField]) -> Void {
        guard let last = fields.last else {
            return
        }
        for i in 0 ..< fields.count - 1 {
            fields[i].returnKeyType = .Next
            fields[i].addTarget(fields[i+1], action: "becomeFirstResponder", forControlEvents: .EditingDidEndOnExit)
        }
        last.returnKeyType = .Done
        last.addTarget(last, action: #selector(UIResponder.resignFirstResponder), forControlEvents: .EditingDidEndOnExit)
    }
}

Thật dễ dàng để sử dụng:

UITextField.connectFields([field1, field2, field3])

Tiện ích mở rộng sẽ đặt nút quay lại thành "Tiếp theo" cho tất cả trừ trường cuối cùng và thành "Xong" cho trường cuối cùng và chuyển trọng tâm / loại bỏ bàn phím khi gõ phím này.

Swift <2.3

extension UITextField {
    class func connectFields(fields:[UITextField]) -> Void {
        guard let last = fields.last else {
            return
        }
        for var i = 0; i < fields.count - 1; i += 1 {
            fields[i].returnKeyType = .Next
            fields[i].addTarget(fields[i+1], action: "becomeFirstResponder", forControlEvents: .EditingDidEndOnExit)
        }
        last.returnKeyType = .Done
        last.addTarget(last, action: "resignFirstResponder", forControlEvents: .EditingDidEndOnExit)
    }
}

SWift 3: sử dụng như thế này -

UITextField.connectFields(fields: [field1, field2])

Extension:
    extension UITextField {
        class func connectFields(fields:[UITextField]) -> Void {
            guard let last = fields.last else {
                return
            }
            for i in 0 ..< fields.count - 1 {
                fields[i].returnKeyType = .next
                fields[i].addTarget(fields[i+1], action: #selector(UIResponder.becomeFirstResponder), for: .editingDidEndOnExit)
            }
            last.returnKeyType = .go
            last.addTarget(last, action: #selector(UIResponder.resignFirstResponder), for: .editingDidEndOnExit)
        }
    }

5
BTW, nếu bạn muốn thực hiện xong một hành động, bạn có thể có Trình điều khiển xem tuân thủ UITextFieldDelegatevà thực hiện func textFieldDidEndEditing(textField: UITextField). Chỉ cần trở về sớm nếu textField != field3.
Ryan

25

Một cách phù hợp và mạnh mẽ hơn là sử dụng NextResponderTextField Bạn có thể định cấu hình nó hoàn toàn từ trình tạo giao diện mà không cần thiết lập ủy nhiệm hoặc sử dụng view.tag.

Tất cả bạn cần làm là

  1. Đặt loại lớp của bạn UITextFieldNextResponderTextField nhập mô tả hình ảnh ở đây
  2. Sau đó đặt đầu ra của nextResponderFieldđiểm để trỏ tới bộ phản hồi tiếp theo, nó có thể là bất cứ thứ gì UITextFieldhoặc bất kỳ UIResponderlớp con nào . Nó cũng có thể là một UIButton và thư viện đủ thông minh để kích hoạt TouchUpInsidesự kiện của nút chỉ khi nó được bật. nhập mô tả hình ảnh ở đây nhập mô tả hình ảnh ở đây

Đây là thư viện đang hoạt động:

nhập mô tả hình ảnh ở đây


Đúng. Hoạt động thực sự tốt với Swift 3.1, giải pháp rất thanh lịch, chỉ là cách IB nên hoạt động tốt.
Richard

Xin lỗi, tôi không nhận được câu hỏi.
mohamede1945

1
NextResponderTextField làm việc cho tôi. Nó rất nhanh và dễ sử dụng, chỉ cần một tập tin nhanh chóng và làm chính xác những gì tôi cần mà không phải lo lắng.
Pat McG

Điều này làm việc tuyệt vời! Lưu ý: Tôi đã phải thay đổi phím Return theo cách thủ công để làm cho nó hiển thị "Tiếp theo" và "Xong" như trong ví dụ.
Mike Miller

14

Tôi thích các giải pháp OO đã được đề xuất bởi Anth0 và answerbot. Tuy nhiên, tôi đã làm việc trên một POC nhanh và nhỏ, vì vậy tôi không muốn làm lộn xộn mọi thứ với các lớp con và danh mục.

Một giải pháp đơn giản khác là tạo NSArray của các trường và tra cứu trường tiếp theo khi bạn nhấn next. Không phải là một giải pháp OO, nhưng nhanh chóng, đơn giản và dễ thực hiện. Ngoài ra, bạn có thể xem và sửa đổi thứ tự trong nháy mắt.

Đây là mã của tôi (được xây dựng dựa trên các câu trả lời khác trong chuỗi này):

@property (nonatomic) NSArray *fieldArray;

- (void)viewDidLoad {
    [super viewDidLoad];

    fieldArray = [NSArray arrayWithObjects: firstField, secondField, thirdField, nil];
}

- (BOOL) textFieldShouldReturn:(UITextField *) textField {
    BOOL didResign = [textField resignFirstResponder];
    if (!didResign) return NO;

    NSUInteger index = [self.fieldArray indexOfObject:textField];
    if (index == NSNotFound || index + 1 == fieldArray.count) return NO;

    id nextField = [fieldArray objectAtIndex:index + 1];
    activeField = nextField;
    [nextField becomeFirstResponder];

    return NO;
}
  • Tôi luôn trả về KHÔNG vì tôi không muốn ngắt dòng. Chỉ cần nghĩ rằng tôi đã chỉ ra rằng từ khi tôi trở về CÓ, nó sẽ tự động thoát khỏi các trường tiếp theo hoặc chèn ngắt dòng trong TextView của tôi. Tôi phải mất một chút thời gian để tìm ra điều đó.
  • activeField theo dõi trường hoạt động trong trường hợp cuộn là cần thiết để không che khuất trường khỏi bàn phím. Nếu bạn có mã tương tự, hãy đảm bảo bạn chỉ định activeField trước khi thay đổi phản hồi đầu tiên. Thay đổi phản hồi đầu tiên là ngay lập tức và sẽ kích hoạt sự kiện KeyboardWasShown ngay lập tức.

Đẹp! Đơn giản và mã là tất cả trong một mô hình. AnthO cũng rất đẹp.
Seamus

Hãy nhớ rằng bạn đang có nguy cơ giữ lại chu kỳ với mã này. FieldArray của bạn đang chuyển đổi các tham chiếu yếu được sử dụng trong tất cả các IBOutlets của bạn sang các tham chiếu mạnh trong chính mảng đó.
Michael Long

11

Đây là một cách thực hiện lập bảng bằng cách sử dụng một danh mục trên UIControl. Giải pháp này có tất cả các ưu điểm của các phương pháp từ Michael và Anth0, nhưng hoạt động cho tất cả các UIControls, không chỉUITextField s. Nó cũng hoạt động trơn tru với Interface Builder và bảng phân cảnh.

Ứng dụng nguồn và mẫu: Kho lưu trữ GitHub cho UIControlsWithTabbing

Sử dụng:

- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
    [textField transferFirstResponderToNextControl];
    return NO;
}

Chỉ định nextControl trong Trình tạo giao diện

Tiêu đề:

//
// UIControl+NextControl.h
// UIControlsWithTabbing
//

#import <UIKit/UIKit.h>

@interface UIControl (NextControl)

@property (nonatomic, weak) IBOutlet UIControl *nextControl;

- (BOOL)transferFirstResponderToNextControl;

@end

Thực hiện:

#import "UIControl+NextControl.h"
#import <objc/runtime.h>

static char defaultHashKey;

@implementation UIControl (NextControl)

- (UIControl *)nextControl
{
    return objc_getAssociatedObject(self, &defaultHashKey);
}

- (void)setNextControl:(UIControl *)nextControl
{
    objc_setAssociatedObject(self, &defaultHashKey, nextControl, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)transferFirstResponderToNextControl
{
    if (self.nextControl)
    {
        [self.nextControl becomeFirstResponder];

        return YES;
    }

    [self resignFirstResponder];

    return NO;
}

@end

1
Đôi khi, tiện ích tiếp theo không phải là điều khiển, tức là UITextView và bạn cũng có thể muốn tab ở đó. Xem xét thay đổi loại từ UITextField sang UIResponder.
Pedro Borges

1
Hoạt động tuyệt vời và với tài liệu tuyệt vời. Tôi ước rằng tôi cuộn xuống câu hỏi này vài giờ trước. :)
Javier Cadiz

10

Tôi đã thử nhiều mã và cuối cùng, điều này hiệu quả với tôi trong Swift 3.0 Mới nhất [Tháng 3 năm 2017]

Các ViewControllerlớp nên được thừa hưởng UITextFieldDelegateđể làm mã làm việc này.

class ViewController: UIViewController,UITextFieldDelegate  

Thêm trường Văn bản với số Thẻ thích hợp và số thẻ này được sử dụng để điều khiển trường văn bản phù hợp dựa trên số thẻ tăng dần được gán cho nó.

override func viewDidLoad() {
    userNameTextField.delegate = self
    userNameTextField.tag = 0
    userNameTextField.returnKeyType = UIReturnKeyType.next
    passwordTextField.delegate = self
    passwordTextField.tag = 1
    passwordTextField.returnKeyType = UIReturnKeyType.go
}

Trong đoạn mã trên, vị returnKeyType = UIReturnKeyType.nexttrí sẽ làm cho phím trở về Bàn phím hiển thị vì Nextbạn cũng có các tùy chọn khác Join/Go, v.v., dựa trên ứng dụng của bạn thay đổi các giá trị.

Đây textFieldShouldReturnlà một phương pháp của UITextFieldDelegate được kiểm soát và ở đây chúng tôi có lựa chọn trường tiếp theo dựa trên sự gia tăng giá trị Thẻ

func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    if let nextField = textField.superview?.viewWithTag(textField.tag + 1) as? UITextField {
        nextField.becomeFirstResponder()
    } else {
        textField.resignFirstResponder()
        return true;
    }
    return false
 }

Điều này hoạt động, ngoại trừ nếu trường đầu vào là UITextView. Bất kỳ ý tưởng làm thế nào để làm điều này kết hợp?
T9b

@ T9b Chính xác bạn cần gì? bạn đang sử dụng loại điều khiển nào trong ứng dụng của mình?
BHUVANESH MOHANKUMAR

Có hai loại đầu vào văn bản, đầu tiên là UITextField(nhập một dòng văn bản), loại còn lại là UITextView(nhập nhiều dòng văn bản). UITextViewrõ ràng là không đáp ứng với mã này nhưng nó có thể được sử dụng trong các biểu mẫu.
T9b

Có, phần mã này chỉ dành cho UITextField và bạn đã thử sửa đổi sự kiện cho UITextView chưa?
BHUVANESH MOHANKUMAR

9

Sau khi bạn thoát khỏi một trường văn bản, bạn gọi [otherTextField trở thànhFirstResponder] và trường tiếp theo sẽ được lấy nét.

Đây thực sự có thể là một vấn đề khó giải quyết vì thường bạn cũng sẽ muốn cuộn màn hình hoặc điều chỉnh vị trí của trường văn bản để dễ dàng nhìn thấy khi chỉnh sửa. Chỉ cần đảm bảo thực hiện nhiều thử nghiệm với việc vào và ra khỏi các trường văn bản theo các cách khác nhau và cũng rời đi sớm (luôn cung cấp cho người dùng tùy chọn để tắt bàn phím thay vì đi đến trường tiếp theo, thường là "Hoàn thành" trong thanh điều hướng)


9
 -(BOOL)textFieldShouldReturn:(UITextField *)textField
{
   [[self.view viewWithTag:textField.tag+1] becomeFirstResponder];
   return YES;
}

Bạn không nên sử dụng thẻ cho mục đích này.
Christian Schnorr

7

Một phương pháp rất dễ dàng để tắt bàn phím khi nhấn nút 'Xong' là:

Tạo một IBAction mới trong tiêu đề

- (IBAction)textFieldDoneEditing:(id)sender;

Trong tệp thực hiện (tệp .m) thêm phương thức sau:

- (IBAction)textFieldDoneEditing:(id)sender 
{ 
  [sender resignFirstResponder];
}

Sau đó, khi bạn đến để liên kết IBAction với trường văn bản - liên kết đến sự kiện 'Đã kết thúc khi thoát'.


1
Bạn được chào đón, nó không điều hướng qua các trường văn bản nhưng nó đóng bàn phím và cho phép bạn chọn một trường khác.
jcrowson

7

Đầu tiên đặt phím trả về bàn phím bằng xib, nếu không bạn có thể viết mã bằng viewdidload:

passWord.returnKeyType = UIReturnKeyNext;

-(BOOL)textFieldShouldReturn:(UITextField *)textField
{
    if(textField == eMail) {
        [textField resignFirstResponder];
        [userName becomeFirstResponder];
    }
    if (textField==userName) {
        [textField resignFirstResponder];
        [passWord becomeFirstResponder];
    }
    if (textField==passWord) {
        [textField resignFirstResponder];
        [country becomeFirstResponder];
    }
    if (textField==country) {
        [textField resignFirstResponder];
    }
    return YES;
}

7

Tôi ngạc nhiên bởi có bao nhiêu câu trả lời ở đây không hiểu được một khái niệm đơn giản: điều hướng qua các điều khiển trong ứng dụng của bạn không phải là điều mà chính các quan điểm nên làm. Đó là công việc của bộ điều khiển để quyết định điều khiển nào để thực hiện phản hồi đầu tiên tiếp theo.

Ngoài ra hầu hết các câu trả lời chỉ áp dụng để điều hướng về phía trước, nhưng người dùng cũng có thể muốn đi ngược lại.

Vì vậy, đây là những gì tôi đã đưa ra. Biểu mẫu của bạn phải được quản lý bởi bộ điều khiển xem và bộ điều khiển xem là một phần của chuỗi phản hồi. Vì vậy, bạn hoàn toàn tự do thực hiện các phương pháp sau:

#pragma mark - Key Commands

- (NSArray *)keyCommands
{
    static NSArray *commands;

    static dispatch_once_t once;
    dispatch_once(&once, ^{
        UIKeyCommand *const forward = [UIKeyCommand keyCommandWithInput:@"\t" modifierFlags:0 action:@selector(tabForward:)];
        UIKeyCommand *const backward = [UIKeyCommand keyCommandWithInput:@"\t" modifierFlags:UIKeyModifierShift action:@selector(tabBackward:)];

        commands = @[forward, backward];
    });

    return commands;
}

- (void)tabForward:(UIKeyCommand *)command
{
    NSArray *const controls = self.controls;
    UIResponder *firstResponder = nil;

    for (UIResponder *const responder in controls) {
        if (firstResponder != nil && responder.canBecomeFirstResponder) {
            [responder becomeFirstResponder]; return;
        }
        else if (responder.isFirstResponder) {
            firstResponder = responder;
        }
    }

    [controls.firstObject becomeFirstResponder];
}

- (void)tabBackward:(UIKeyCommand *)command
{
    NSArray *const controls = self.controls;
    UIResponder *firstResponder = nil;

    for (UIResponder *const responder in controls.reverseObjectEnumerator) {
        if (firstResponder != nil && responder.canBecomeFirstResponder) {
            [responder becomeFirstResponder]; return;
        }
        else if (responder.isFirstResponder) {
            firstResponder = responder;
        }
    }

    [controls.lastObject becomeFirstResponder];
}

Logic bổ sung để cuộn các phản hồi ngoài màn hình có thể nhìn thấy trước có thể được áp dụng.

Một ưu điểm khác của phương pháp này là bạn không cần phải phân lớp tất cả các loại điều khiển mà bạn có thể muốn hiển thị (như UITextFields) mà thay vào đó có thể quản lý logic ở cấp điều khiển, trong đó, hãy trung thực, là nơi thích hợp để làm như vậy .


Tôi tìm thấy câu trả lời của bạn về mức độ phù hợp nhất, nhưng tôi đã nhận thấy một điều thú vị mà có lẽ bạn có thể làm rõ. Khi sử dụng UITextFields không được quản lý bởi UIViewControll, nhấn phím tab sẽ làm cho UITextField tiếp theo trở thành phản hồi đầu tiên theo mặc định. Tuy nhiên, khi từng được quản lý bởi một VC, điều này không đúng. VC của tôi không làm gì cả, nhưng chức năng lập bảng được tích hợp đã biến mất. Tôi đã nhận thấy rằng KeyCommands của VC cuối cùng được thêm vào chuỗi phản hồi của chế độ xem. Điều này khiến tôi tin rằng nếu bạn đang sử dụng một VC, thì bắt buộc phải triển khai keyCommands.
Joey Carson

4

Tôi đã thêm vào câu trả lời của PeyloW trong trường hợp bạn đang muốn thực hiện chức năng nút trước đó / tiếp theo:

- (IBAction)moveThroughTextFields:(UIBarButtonItem *)sender 
{
    NSInteger nextTag;
    UITextView *currentTextField = [self.view findFirstResponderAndReturn];

    if (currentTextField != nil) {
        // I assigned tags to the buttons.  0 represent prev & 1 represents next
        if (sender.tag == 0) {
            nextTag = currentTextField.tag - 1;

        } else if (sender.tag == 1) {
            nextTag = currentTextField.tag + 1;
        }
    }
    // Try to find next responder
    UIResponder* nextResponder = [self.view viewWithTag:nextTag];
    if (nextResponder) {
        // Found next responder, so set it.
        // I added the resign here in case there's different keyboards in place.
        [currentTextField resignFirstResponder];
        [nextResponder becomeFirstResponder];
    } else {
        // Not found, so remove keyboard.
        [currentTextField resignFirstResponder];

    }
}

Nơi bạn phân lớp UIView như thế này:

@implementation UIView (FindAndReturnFirstResponder)
- (UITextView *)findFirstResponderAndReturn
{
    for (UITextView *subView in self.subviews) {
        if (subView.isFirstResponder){
            return subView;
        }
    }
    return nil;
}
@end

4

Xin chào mọi người, xin hãy xem cái này

- (void)nextPrevious:(id)sender
{

  UIView *responder = [self.view findFirstResponder];   

  if (nil == responder || ![responder isKindOfClass:[GroupTextField class]]) {
    return;
  }

  switch([(UISegmentedControl *)sender selectedSegmentIndex]) {
    case 0:
      // previous
      if (nil != ((GroupTextField *)responder).previousControl) {
        [((GroupTextField *)responder).previousControl becomeFirstResponder];
        DebugLog(@"currentControl: %i previousControl: %i",((GroupTextField *)responder).tag,((GroupTextField *)responder).previousControl.tag);
      }
      break;
    case 1:
      // next
      if (nil != ((GroupTextField *)responder).nextControl) {
        [((GroupTextField *)responder).nextControl becomeFirstResponder];
        DebugLog(@"currentControl: %i nextControl: %i",((GroupTextField *)responder).tag,((GroupTextField *)responder).nextControl.tag);
      }     
      break;    
  }
}

Thay vì trả lời UIView *, sẽ chính xác hơn khi sử dụng UIRespoder * trả lời
Pedro Borges

4

Tôi đã cố gắng giải quyết vấn đề này bằng cách sử dụng một cách tiếp cận tinh vi hơn dựa trên việc gán từng ô (hoặc UITextField) trong một UITableViewgiá trị thẻ duy nhất có thể được truy xuất sau này: activ-next-uitextfield-in-uitableview-ios

Tôi hi vọng cái này giúp được!


4

Tôi vừa tạo Pod mới khi xử lý công cụ này GNTextFieldsCollectionManager . Nó tự động xử lý sự cố textField tiếp theo / cuối cùng và rất dễ sử dụng:

[[GNTextFieldsCollectionManager alloc] initWithView:self.view];

Lấy tất cả các trường văn bản được sắp xếp bằng cách xuất hiện trong cấu trúc phân cấp xem (hoặc theo thẻ) hoặc bạn có thể chỉ định mảng textField của riêng mình.


4

Giải pháp trong Swift 3.1, Sau khi kết nối các trường văn bản của bạn, IBOutlets đặt đại biểu trường văn bản của bạn trong viewDidLoad, và sau đó điều hướng hành động của bạn trong textFieldShouldReturn

class YourViewController: UIViewController,UITextFieldDelegate {

        @IBOutlet weak var passwordTextField: UITextField!
        @IBOutlet weak var phoneTextField: UITextField!

        override func viewDidLoad() {
            super.viewDidLoad()
            self.passwordTextField.delegate = self
            self.phoneTextField.delegate = self
            // Set your return type
            self.phoneTextField.returnKeyType = .next
            self.passwordTextField.returnKeyType = .done
        }

        func textFieldShouldReturn(_ textField: UITextField) -> Bool{
            if textField == self.phoneTextField {
                self.passwordTextField.becomeFirstResponder()
            }else if textField == self.passwordTextField{
                // Call login api
                self.login()
            }
            return true
        }

    }

4

Nếu ai đó muốn như thế này. Tôi nghĩ rằng đây là gần nhất với các yêu cầu được yêu cầu trong câu hỏi

nhập mô tả hình ảnh ở đây

Đây là cách tôi đã thực hiện điều này

Thêm chế độ xem phụ kiện cho từng trường văn bản mà bạn muốn thiết lập, sử dụng

func setAccessoryViewFor(textField : UITextField)    {
    let toolBar = UIToolbar()
    toolBar.barStyle = .default
    toolBar.isTranslucent = true
    toolBar.sizeToFit()

    // Adds the buttons

    // Add previousButton
    let prevButton = UIBarButtonItem(title: "<", style: .plain, target: self, action: #selector(previousPressed(sender:)))
    prevButton.tag = textField.tag
    if getPreviousResponderFor(tag: textField.tag) == nil {
        prevButton.isEnabled = false
    }

    // Add nextButton
    let nextButton = UIBarButtonItem(title: ">", style: .plain, target: self, action: #selector(nextPressed(sender:)))
    nextButton.tag = textField.tag
    if getNextResponderFor(tag: textField.tag) == nil {
        nextButton.title = "Done"
    }

    let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
    toolBar.setItems([prevButton,spaceButton,nextButton], animated: false)
    toolBar.isUserInteractionEnabled = true
    textField.inputAccessoryView = toolBar
}

Sử dụng các chức năng sau để xử lý vòi

func nextPressed(sender : UIBarButtonItem) {
    if let nextResponder = getNextResponderFor(tag: sender.tag) {
        nextResponder.becomeFirstResponder()
    } else {
        self.view.endEditing(true)
    }

}

func previousPressed(sender : UIBarButtonItem) {
    if let previousResponder = getPreviousResponderFor(tag : sender.tag)  {
        previousResponder.becomeFirstResponder()
    }
}

func getNextResponderFor(tag : Int) -> UITextField? {
    return self.view.viewWithTag(tag + 1) as? UITextField
}

func getPreviousResponderFor(tag : Int) -> UITextField? {
    return self.view.viewWithTag(tag - 1) as? UITextField
}

Bạn sẽ cần cung cấp các thẻ textFields theo thứ tự mà bạn muốn nút tiếp theo / trước phản hồi.


3

Tôi thích hơn:

@interface MyViewController : UIViewController
@property (nonatomic, retain) IBOutletCollection(UIView) NSArray *inputFields;
@end

Trong tệp NIB, tôi nối textFields theo thứ tự mong muốn vào mảng inputFields này. Sau đó, tôi thực hiện một thử nghiệm đơn giản cho chỉ mục của UITextField báo cáo rằng người dùng đã khai thác trả về:

// for UITextField
-(BOOL)textFieldShouldReturn:(UITextField*)textField {
    NSUInteger index = [_inputFields indexOfObject:textField];
    index++;
    if (index < _inputFields.count) {
        UIView *v = [_inputFields objectAtIndex:index];
        [v becomeFirstResponder];
    }
    return NO;
}

// for UITextView
-(BOOL)textView:(UITextView*)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString*)text {
    if ([@"\n" isEqualToString:text]) {
        NSUInteger index = [_inputFields indexOfObject:textView];
        index++;
        if (index < _inputFields.count) {
            UIView *v = [_inputFields objectAtIndex:index];
            [v becomeFirstResponder];
        } else {
            [self.view endEditing:YES];
        }
        return NO;
    }
    return YES;
}

3
if (ô == nil)
{
    cell = [[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault reuseIdentifier: cellIdentifier];
    txt_Input = [[UITextField alloc] initWithFrame: CGRectMake (0, 10, 150, 30)];
    txt_Input.tag = indexPath.row + 1;
    [self.array_Textfields addObject: txt_Input]; // Khởi tạo mảng có thể thay đổi trong ViewDidLoad
}

- (BOOL) textFieldShouldReturn: (UITextField *) textField
{

    int tag = (int) textField.tag;
    UITextField * txt = [self.array_Textfields objectAt Index: tag];
    [txt trở thành FirstResponder];
    trả lại CÓ;
}

3

Tôi đã có khoảng hơn 10 UITextField trong bảng câu chuyện của mình và cách tôi kích hoạt chức năng tiếp theo là bằng cách tạo một mảng UITextField và biến UITextField tiếp theo thành phản hồi đầu tiên. Đây là tập tin thực hiện:

#import "RegistrationTableViewController.h"

@interface RegistrationTableViewController ()
@property (weak, nonatomic) IBOutlet UITextField *fullNameTextField;
@property (weak, nonatomic) IBOutlet UITextField *addressTextField;
@property (weak, nonatomic) IBOutlet UITextField *address2TextField;
@property (weak, nonatomic) IBOutlet UITextField *cityTextField;
@property (weak, nonatomic) IBOutlet UITextField *zipCodeTextField;
@property (weak, nonatomic) IBOutlet UITextField *urlTextField;
@property (weak, nonatomic) IBOutlet UITextField *usernameTextField;
@property (weak, nonatomic) IBOutlet UITextField *emailTextField;
@property (weak, nonatomic) IBOutlet UITextField *passwordTextField;
@property (weak, nonatomic) IBOutlet UITextField *confirmPWTextField;

@end
NSArray *uiTextFieldArray;
@implementation RegistrationTableViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"view did load");
    uiTextFieldArray = @[self.fullNameTextField,self.addressTextField,self.address2TextField,self.cityTextField,self.zipCodeTextField,self.urlTextField,self.usernameTextField,self.emailTextField,self.passwordTextField,self.confirmPWTextField];
    for(UITextField *myField in uiTextFieldArray){
        myField.delegate = self;
    }


}
-(BOOL)textFieldShouldReturn:(UITextField *)textField{
    long index = [uiTextFieldArray indexOfObject:textField];
    NSLog(@"%ld",index);
    if(index < (uiTextFieldArray.count - 1)){
        [uiTextFieldArray[++index] becomeFirstResponder];
    }else{
        [uiTextFieldArray[index] resignFirstResponder];
    }
    return YES;
}
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

@end

3

Điều này làm việc cho tôi trong Xamarin.iOS / Monotouch. Thay đổi nút bàn phím thành Tiếp theo, chuyển điều khiển sang UITextField tiếp theo và ẩn bàn phím sau UITextField cuối cùng.

private void SetShouldReturnDelegates(IEnumerable<UIView> subViewsToScout )
{
  foreach (var item in subViewsToScout.Where(item => item.GetType() == typeof (UITextField)))
  {
    (item as UITextField).ReturnKeyType = UIReturnKeyType.Next;
    (item as UITextField).ShouldReturn += (textField) =>
    {
        nint nextTag = textField.Tag + 1;
        var nextResponder = textField.Superview.ViewWithTag(nextTag);
        if (null != nextResponder)
            nextResponder.BecomeFirstResponder();
        else
            textField.Superview.EndEditing(true); 
            //You could also use textField.ResignFirstResponder(); 

        return false; // We do not want UITextField to insert line-breaks.
    };
  }
}

Bên trong ViewDidLoad bạn sẽ có:

Nếu TextFields của bạn chưa có Thẻ, hãy đặt nó ngay bây giờ:

txtField1.Tag = 0;
txtField2.Tag = 1;
txtField3.Tag = 2;
//...

và chỉ cuộc gọi

SetShouldReturnDelegates(yourViewWithTxtFields.Subviews.ToList());
//If you are not sure of which view contains your fields you can also call it in a safer way:
SetShouldReturnDelegates(txtField1.Superview.Subviews.ToList());
//You can also reuse the same method with different containerViews in case your UITextField are under different views.

3

Đây là một giải pháp đơn giản trong swift, không sử dụng thẻ, không có thủ thuật bảng phân cảnh ...

Chỉ cần sử dụng tiện ích mở rộng này:

extension UITextField{

    func nextTextFieldField() -> UITextField?{
        //field to return
        var returnField : UITextField?
        if self.superview != nil{
            //for each view in superview
            for (_, view) in self.superview!.subviews.enumerate(){
                //if subview is a text's field
                if view.isKindOfClass(UITextField){
                    //cast curent view as text field
                    let currentTextField = view as! UITextField
                    //if text field is after the current one
                    if currentTextField.frame.origin.y > self.frame.origin.y{
                        //if there is no text field to return already
                        if returnField == nil {
                            //set as default return
                            returnField = currentTextField
                        }
                            //else if this this less far than the other
                        else if currentTextField.frame.origin.y < returnField!.frame.origin.y{
                            //this is the field to return
                            returnField = currentTextField
                        }
                    }
                }
            }
        }
        //end of the mdethod
        return returnField
    }

}

Và gọi nó như thế này (ví dụ) với đại biểu trường văn bản của bạn:

func textFieldShouldReturn(textField: UITextField) -> Bool {
    textField.resignFirstResponder()
    textField.nextTextFieldField()?.becomeFirstResponder()
    return true
}

Một nhận xét về điều này là phương pháp của bạn giả định tất cả các trường được xếp theo chiều dọc trong một giám sát duy nhất. Nó không cho phép các trường được nhóm và không hoạt động khi các trường văn bản có thể nằm cạnh nhau. Điều sau này đặc biệt quan trọng trong những ngày này khi người ta sử dụng các lớp kích thước để sắp xếp lại các biểu mẫu cho chế độ xem ngang và khi các biểu mẫu được hiển thị trên máy tính bảng.
Michael Long

3

Một cách an toàn hơn và trực tiếp hơn, giả sử:

  • các đại biểu trường văn bản được đặt thành trình điều khiển xem của bạn
  • tất cả các trường văn bản là các bản xem trước của cùng một chế độ xem
  • các trường văn bản có các thẻ theo thứ tự bạn muốn tiến triển (ví dụ: textField2.tag = 2, textField3.tag = 3, v.v.)
  • chuyển sang trường văn bản tiếp theo sẽ xảy ra khi bạn nhấn nút quay lại trên bàn phím (bạn có thể thay đổi trường này thành tiếp theo , xong , v.v.)
  • bạn muốn loại bỏ bàn phím sau trường văn bản cuối cùng

Swift 4.1:

extension ViewController: UITextFieldDelegate {
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        let nextTag = textField.tag + 1
        guard let nextTextField = textField.superview?.viewWithTag(nextTag) else {
            textField.resignFirstResponder()
            return false
        }

    nextTextField.becomeFirstResponder()

    return false

    }
}

2

trong textFieldShouldReturn, bạn nên kiểm tra xem trường văn bản bạn đang sử dụng không phải là trường cuối cùng khi họ nhấp vào tiếp theo và nếu nó không tắt bàn phím ..


Này, ok cái này hoạt động, nhưng làm thế nào tôi có thể nhảy đến trường văn bản tiếp theo bằng nút "Tiếp theo"?
phx

2

Đây là một bài viết cũ, nhưng có thứ hạng trang cao vì vậy tôi sẽ đồng ý với giải pháp của mình.

Tôi đã gặp một vấn đề tương tự và cuối cùng đã tạo ra một lớp con UIToolbarđể quản lý chức năng tiếp theo / trước / được thực hiện trong một bảng động Xem với các phần: https://github.com/jday001/DataEntryToolbar

Bạn đặt thanh công cụ là inputAccessoryView của các trường văn bản của bạn và thêm chúng vào từ điển của nó. Điều này cho phép bạn quay vòng qua chúng về phía trước và phía sau, ngay cả với nội dung động. Có các phương thức ủy nhiệm nếu bạn muốn kích hoạt chức năng của chính mình khi điều hướng textField xảy ra, nhưng bạn không phải đối phó với việc quản lý bất kỳ thẻ hoặc trạng thái phản hồi đầu tiên.

Có các đoạn mã & một ứng dụng ví dụ tại liên kết GitHub để trợ giúp với các chi tiết triển khai. Bạn sẽ cần mô hình dữ liệu của riêng mình để theo dõi các giá trị bên trong các trường.


2

Không có thẻ sử dụng và không thêm thuộc tính cho nextField / nextTextField, bạn có thể thử điều này để mô phỏng TAB, trong đó "testInput" là trường hoạt động hiện tại của bạn:

if ([textInput isFirstResponder])
    [textInput.superview.subviews enumerateObjectsAtIndexes:
     [NSIndexSet indexSetWithIndexesInRange:
      NSMakeRange([textInput.superview.subviews indexOfObject:textInput]+1,
                  [textInput.superview.subviews count]-[textInput.superview.subviews indexOfObject:textInput]-1)]
                                                    options:0 usingBlock:^(UIView *obj, NSUInteger idx, BOOL *stop) {
                                                        *stop = !obj.hidden && [obj becomeFirstResponder];
                                                    }];
if ([textInput isFirstResponder])
    [textInput.superview.subviews enumerateObjectsAtIndexes:
     [NSIndexSet indexSetWithIndexesInRange:
      NSMakeRange(0,
                  [textInput.superview.subviews indexOfObject:textInput])]
                                                    options:0 usingBlock:^(UIView *obj, NSUInteger idx, BOOL *stop) {
                                                        *stop = !obj.hidden && [obj becomeFirstResponder];
                                                    }];

2

Tôi đã sử dụng câu trả lời của Michael G. Emmons khoảng một năm nay, hoạt động rất tốt. Gần đây tôi đã nhận thấy rằng việc gọi resignFirstResponder và sau đó trở thànhFirstResponder ngay lập tức có thể khiến bàn phím bị "trục trặc", biến mất và sau đó xuất hiện ngay lập tức. Tôi đã thay đổi phiên bản của anh ấy một chút để bỏ qua resignFirstResponder nếu nextField có sẵn.

- (BOOL) textFieldShouldReturn: (UITextField *) textField
{ 

    if ([textField isKindOfClass: [lớp NRTextField]])
    {
        NRTextField * nText = (NRTextField *) textField;
        if ([nText nextField]! = nil) {
            Clark_async (Clark_get_main_queue (),
                           ^ {[[nText nextField] trở thànhFirstResponder]; });

        }
        khác {
            [textField từ chứcFirstResponder];
        }
    }
    khác {
        [textField từ chứcFirstResponder];
    }

    trả lại đúng sự thật;

}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.