Init tùy chỉnh cho UIViewController trong Swift với thiết lập giao diện trong bảng phân cảnh


89

Tôi đang gặp sự cố khi viết init tùy chỉnh cho lớp con của UIViewController, về cơ bản tôi muốn chuyển phụ thuộc thông qua phương thức init cho viewController hơn là đặt thuộc tính trực tiếp như viewControllerB.property = value

Vì vậy, tôi đã tạo một init tùy chỉnh cho viewController của mình và gọi init siêu được chỉ định

init(meme: Meme?) {
        self.meme = meme
        super.init(nibName: nil, bundle: nil)
    }

Giao diện bộ điều khiển chế độ xem nằm trong bảng phân cảnh, tôi cũng đã tạo giao diện cho lớp tùy chỉnh làm bộ điều khiển chế độ xem của tôi. Và Swift yêu cầu gọi phương thức init này ngay cả khi bạn không làm gì trong phương thức này. Nếu không trình biên dịch sẽ phàn nàn ...

required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

Vấn đề là khi tôi cố gắng gọi init tùy chỉnh của mình với MyViewController(meme: meme)nó không init thuộc tính nào trong viewController của tôi ...

Tôi đang cố gắng gỡ lỗi, tôi thấy trong viewController của mình, init(coder aDecoder: NSCoder)được gọi trước, sau đó đơn vị tùy chỉnh của tôi được gọi sau. Tuy nhiên, hai phương thức init này trả về selfđịa chỉ bộ nhớ khác nhau .

Tôi đang nghi ngờ có điều gì đó không ổn với init cho viewController của mình và nó sẽ luôn trả về selfvới init?(coder aDecoder: NSCoder), mà không có triển khai.

Có ai biết cách tạo init tùy chỉnh cho viewController của bạn một cách chính xác không? Lưu ý: giao diện viewController của tôi được thiết lập trong bảng phân cảnh

đây là mã viewController của tôi:

class MemeDetailVC : UIViewController {

    var meme : Meme!

    @IBOutlet weak var editedImage: UIImageView!

    // TODO: incorrect init
    init(meme: Meme?) {
        self.meme = meme
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func viewDidLoad() {
        /// setup nav title
        title = "Detail Meme"

        super.viewDidLoad()
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        editedImage = UIImageView(image: meme.editedImage)
    }

}

bạn đã nhận được một giải pháp cho điều này?
user481610

Câu trả lời:


49

Vì nó đã được chỉ định trong một trong các câu trả lời ở trên, bạn không thể sử dụng cả phương thức init tùy chỉnh và bảng phân cảnh. Nhưng bạn vẫn có thể sử dụng một phương thức tĩnh để khởi tạo ViewController từ một bảng phân cảnh và thực hiện thiết lập bổ sung trên đó. Nó sẽ trông giống thế này:

class MemeDetailVC : UIViewController {

    var meme : Meme!

    static func makeMemeDetailVC(meme: Meme) -> MemeDetailVC {
        let newViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("IdentifierOfYouViewController") as! MemeDetailVC

        newViewController.meme = meme

        return newViewController
    }
}

Đừng quên chỉ định IdfierOfYouViewController làm định danh bộ điều khiển chế độ xem trong bảng phân cảnh của bạn. Bạn cũng có thể cần thay đổi tên của bảng phân cảnh trong mã ở trên.


Sau khi MemeDetailVC được khởi tạo, thuộc tính "meme" sẽ tồn tại. Sau đó, tiêm phụ thuộc vào để gán giá trị cho "meme". Tại thời điểm này, loadView () và viewDidLoad chưa được gọi. Hai phương thức đó sẽ được gọi sau khi MemeDetailVC thêm / đẩy để xem hệ thống phân cấp.
Jim Yu,

Câu trả lời hay nhất ở đây!
PascalS

Tuy nhiên phương pháp init là cần thiết, và trình biên dịch sẽ nói meme tài sản lưu trữ chưa được khởi tạo
Surendra Kumar

27

Bạn không thể sử dụng trình khởi tạo tùy chỉnh khi khởi tạo từ Bảng phân cảnh, đó init?(coder aDecoder: NSCoder)là cách Apple thiết kế bảng phân cảnh để khởi tạo bộ điều khiển. Tuy nhiên, có nhiều cách để gửi dữ liệu đến a UIViewController.

Tên bộ điều khiển chế độ xem của bạn có detailtrong đó, vì vậy tôi cho rằng bạn đến đó từ một bộ điều khiển khác. Trong trường hợp này, bạn có thể sử dụng prepareForSeguephương thức để gửi dữ liệu đến chi tiết (Đây là Swift 3):

override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "identifier" {
        if let controller = segue.destinationViewController as? MemeDetailVC {
            controller.meme = "Meme"
        }
    }
}

Tôi chỉ sử dụng một thuộc tính loại Stringthay vì Memecho mục đích thử nghiệm. Ngoài ra, hãy đảm bảo rằng bạn nhập đúng định danh segue ( "identifier"chỉ là một trình giữ chỗ).


1
Xin chào, nhưng làm thế nào chúng ta có thể loại bỏ việc chỉ định như một tùy chọn và chúng tôi cũng không muốn nhầm lẫn khi không gán nó. Chúng tôi chỉ muốn sự phụ thuộc chặt chẽ có ý nghĩa đối với bộ điều khiển tồn tại ngay từ đầu.
Amber K

1
@AmberK Bạn có thể muốn xem xét lập trình giao diện của mình thay vì sử dụng Trình tạo giao diện.
Caleb Kleveter

Được rồi, tôi cũng đang xem dự án kickstarter ios để tham khảo, vẫn chưa thể biết họ gửi mô hình xem của họ như thế nào.
Amber K

23

Như @Caleb Kleveter đã chỉ ra, chúng tôi không thể sử dụng trình khởi tạo tùy chỉnh trong khi khởi tạo từ Bảng phân cảnh.

Nhưng, chúng ta có thể giải quyết vấn đề bằng cách sử dụng phương thức factory / class để khởi tạo đối tượng bộ điều khiển chế độ xem từ Bảng phân cảnh và trả về đối tượng bộ điều khiển chế độ xem. Tôi nghĩ đây là một cách khá hay.

Lưu ý: Đây không phải là một câu trả lời chính xác cho câu hỏi mà là một cách giải quyết vấn đề.

Tạo phương thức lớp, trong lớp MemeDetailVC, như sau:

// Considering your view controller resides in Main.storyboard and it's identifier is set to "MemeDetailVC"
class func `init`(meme: Meme) -> MemeDetailVC? {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let vc = storyboard.instantiateViewController(withIdentifier: "MemeDetailVC") as? MemeDetailVC
    vc?.meme = meme
    return vc
}

Sử dụng:

let memeDetailVC = MemeDetailVC.init(meme: Meme())

5
Nhưng vẫn còn hạn chế là ở đó, tài sản phải là tùy chọn.
Amber K

khi sử dụng class func init (meme: Meme) -> MemeDetailVCXcode 10.2 bị nhầm lẫn và xuất hiện lỗiIncorrect argument label in call (have 'meme:', expected 'coder:')
Samuël

21

Một cách mà tôi đã thực hiện là với một trình khởi tạo tiện lợi.

class MemeDetailVC : UIViewController {

    convenience init(meme: Meme) {
        self.init()
        self.meme = meme
    }
}

Sau đó, bạn khởi tạo MemeDetailVC của mình bằng let memeDetailVC = MemeDetailVC(theMeme)

Tài liệu của Apple về trình khởi tạo khá tốt, nhưng yêu thích của cá nhân tôi là loạt hướng dẫn Ray Wenderlich: Khởi tạo trong độ sâu sẽ cung cấp cho bạn nhiều giải thích / ví dụ về các tùy chọn init khác nhau của bạn và cách "thích hợp" để thực hiện.


CHỈNH SỬA : Mặc dù bạn có thể sử dụng trình khởi tạo tiện lợi trên bộ điều khiển chế độ xem tùy chỉnh, nhưng mọi người đều đúng khi nói rằng bạn không thể sử dụng trình khởi tạo tùy chỉnh khi khởi tạo từ bảng phân cảnh hoặc thông qua một phân cảnh bảng phân cảnh.

Nếu giao diện của bạn được thiết lập trong bảng phân cảnh và bạn đang tạo bộ điều khiển hoàn toàn theo lập trình, thì bộ khởi tạo tiện lợi có lẽ là cách dễ dàng nhất để thực hiện những gì bạn đang cố gắng làm vì bạn không phải xử lý với init bắt buộc NSCoder (mà tôi vẫn chưa thực sự hiểu).

Tuy nhiên, nếu bạn đang nhận bộ điều khiển chế độ xem của mình thông qua bảng phân cảnh, thì bạn sẽ cần làm theo câu trả lời của @Caleb Kleveter và truyền bộ điều khiển chế độ xem vào lớp con mong muốn của bạn, sau đó đặt thuộc tính theo cách thủ công.


1
Tôi không hiểu, nếu giao diện của tôi được thiết lập trong bảng phân cảnh và bạn đang tạo nó theo chương trình, thì tôi phải gọi phương thức khởi tạo bằng bảng phân cảnh để tải giao diện đúng không? Làm thế nào sẽ convenienceđược giúp đỡ ở đây.
Amber K

Các lớp trong swift không thể được khởi tạo bằng cách gọi một inithàm khác của lớp (tuy nhiên có thể cấu trúc). Vì vậy, để gọi self.init(), bạn phải đánh dấu init(meme: Meme)làm conveniencekhởi tạo. Nếu không, bạn sẽ phải tự đặt tất cả các thuộc tính bắt buộc của một UIViewControllertrong trình khởi tạo theo cách thủ công và tôi không chắc tất cả các thuộc tính đó là gì.
Ponyboy47

thì đây không phải là phân lớp UIViewController, vì bạn đang gọi self.init () chứ không phải super.init (). bạn vẫn có thể khởi tạo MemeDetailVC bằng cách sử dụng init như mặc định MemeDetail()và trong trường hợp đó, mã sẽ bị lỗi
Ali

Nó vẫn được coi là lớp con vì self.init () sẽ phải gọi super.init () trong quá trình triển khai của nó hoặc có lẽ self.init () được kế thừa trực tiếp từ lớp cha trong trường hợp chúng tương đương về mặt chức năng.
Ponyboy47

6

Ban đầu có một vài câu trả lời được bỏ phiếu và bị xóa mặc dù về cơ bản chúng đúng. Câu trả lời là, bạn không thể.

Khi làm việc từ định nghĩa bảng phân cảnh, tất cả các phiên bản trình điều khiển chế độ xem của bạn đều được lưu trữ. Vì vậy, để bắt chúng, nó bắt buộc phải init?(coder...được sử dụng. Đây coderlà nơi bắt nguồn của tất cả thông tin cài đặt / chế độ xem.

Vì vậy, trong trường hợp này, không thể gọi một số hàm init khác với một tham số tùy chỉnh. Nó phải được đặt làm thuộc tính khi chuẩn bị segue hoặc bạn có thể loại bỏ các phân đoạn và tải các phiên bản trực tiếp từ bảng phân cảnh và định cấu hình chúng (về cơ bản là một mẫu ban đầu sử dụng bảng phân cảnh).

Trong mọi trường hợp, bạn sử dụng hàm init bắt buộc của SDK và chuyển các tham số bổ sung sau đó.


4

Swift 5

Bạn có thể viết bộ khởi tạo tùy chỉnh như thế này ->

class MyFooClass: UIViewController {

    var foo: Foo?

    init(with foo: Foo) {
        self.foo = foo
        super.init(nibName: nil, bundle: nil)
    }

    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.foo = nil
    }
}

3

UIViewControllerlớp tuân theo NSCodinggiao thức được định nghĩa là:

public protocol NSCoding {

   public func encode(with aCoder: NSCoder)

   public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER
}    

Vì vậy, UIViewControllercó hai bộ khởi tạo được chỉ định init?(coder aDecoder: NSCoder)init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?).

Storyborad gọi init?(coder aDecoder: NSCoder)trực tiếp đến init UIViewControllerUIView, không có chỗ cho bạn chuyển các tham số.

Một cách giải quyết rườm rà là sử dụng bộ nhớ đệm tạm thời:

class TempCache{
   static let sharedInstance = TempCache()

   var meme: Meme?
}

TempCache.sharedInstance.meme = meme // call this before init your ViewController    

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder);
    self.meme = TempCache.sharedInstance.meme
}


0

Tuyên bố từ chối trách nhiệm: Tôi không ủng hộ điều này và chưa kiểm tra kỹ lưỡng khả năng phục hồi của nó, nhưng đó là một giải pháp tiềm năng mà tôi đã phát hiện ra khi chơi xung quanh.

Về mặt kỹ thuật, có thể đạt được quá trình khởi tạo tùy chỉnh trong khi vẫn duy trì giao diện được định cấu hình bởi bảng phân cảnh bằng cách khởi tạo bộ điều khiển chế độ xem hai lần: lần đầu tiên thông qua tùy chỉnh của bạn initvà lần thứ hai bên trong loadView()nơi bạn xem từ bảng phân cảnh.

final class CustomViewController: UIViewController {
  @IBOutlet private weak var label: UILabel!
  @IBOutlet private weak var textField: UITextField!

  private let foo: Foo!

  init(someParameter: Foo) {
    self.foo = someParameter
    super.init(nibName: nil, bundle: nil)
  }

  override func loadView() {
    //Only proceed if we are not the storyboard instance
    guard self.nibName == nil else { return super.loadView() }

    //Initialize from storyboard
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let storyboardInstance = storyboard.instantiateViewController(withIdentifier: "CustomVC") as! CustomViewController

    //Remove view from storyboard instance before assigning to us
    let storyboardView = storyboardInstance.view
    storyboardInstance.view.removeFromSuperview()
    storyboardInstance.view = nil
    self.view = storyboardView

    //Receive outlet references from storyboard instance
    self.label = storyboardInstance.label
    self.textField = storyboardInstance.textField
  }

  required init?(coder: NSCoder) {
    //Must set all properties intended for custom init to nil here (or make them `var`s)
    self.foo = nil
    //Storyboard initialization requires the super implementation
    super.init(coder: coder)
  }
}

Giờ đây, ở nơi khác trong ứng dụng của bạn, bạn có thể gọi trình khởi tạo tùy chỉnh của mình như CustomViewController(someParameter: foo)và vẫn nhận được cấu hình chế độ xem từ bảng phân cảnh.

Tôi không coi đây là một giải pháp tuyệt vời vì một số lý do:

  • Khởi tạo đối tượng được sao chép, bao gồm bất kỳ thuộc tính pre-init nào
  • Các thông số được chuyển đến tùy chỉnh initphải được lưu trữ dưới dạng thuộc tính tùy chọn
  • Thêm boilerplate phải được duy trì khi các cửa hàng / thuộc tính thay đổi

Có lẽ bạn có thể chấp nhận những đánh đổi này, nhưng hãy tự chịu rủi ro khi sử dụng .


0

Mặc dù bây giờ chúng ta có thể thực hiện init tùy chỉnh cho các bộ điều khiển mặc định trong bảng phân cảnh bằng cách sử dụng instantiateInitialViewController(creator:)và cho các segues bao gồm mối quan hệ và chương trình.

Khả năng này đã được thêm vào Xcode 11 và sau đây là đoạn trích từ Ghi chú phát hành Xcode 11 :

Phương thức bộ điều khiển chế độ xem được chú thích với @IBSegueActionthuộc tính mới có thể được sử dụng để tạo bộ điều khiển chế độ xem đích của segue trong mã, sử dụng trình khởi tạo tùy chỉnh với bất kỳ giá trị bắt buộc nào. Điều này giúp bạn có thể sử dụng bộ điều khiển chế độ xem với yêu cầu khởi tạo không tùy chọn trong bảng phân cảnh. Tạo kết nối từ một segue đến một @IBSegueActionphương thức trên bộ điều khiển chế độ xem nguồn của nó. Trên các phiên bản hệ điều hành mới hỗ trợ Segue Actions, phương thức đó sẽ được gọi và giá trị mà nó trả về sẽ là destinationViewControllercủa đối tượng segue được chuyển đến prepareForSegue:sender:. Nhiều @IBSegueActionphương pháp có thể được xác định trên một bộ điều khiển chế độ xem nguồn duy nhất, điều này có thể giảm bớt nhu cầu kiểm tra các chuỗi định danh segue trong prepareForSegue:sender:. (47091566)

Một IBSegueActionphương thức có tối đa ba tham số: người viết mã, người gửi và mã định danh của người gửi. Tham số đầu tiên là bắt buộc và các tham số khác có thể được bỏ qua khỏi chữ ký phương thức của bạn nếu muốn. Các NSCoderphải đi qua để khởi tạo các điểm điều khiển của đích, để đảm bảo nó được tùy chỉnh với các giá trị cấu hình trong kịch bản này. Phương thức này trả về bộ điều khiển dạng xem phù hợp với loại bộ điều khiển đích được xác định trong bảng phân cảnh hoặc nilđể khởi tạo bộ điều khiển đích bằng init(coder:)phương thức chuẩn . Nếu bạn biết mình không cần trả lại nil, kiểu trả về có thể là không bắt buộc.

Trong Swift, hãy thêm @IBSegueActionthuộc tính:

@IBSegueAction
func makeDogController(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> ViewController? {
    PetController(
        coder: coder,
        petName:  self.selectedPetName, type: .dog
    )
}

Trong Objective-C, thêm IBSegueActionvào trước kiểu trả về:

- (IBSegueAction ViewController *)makeDogController:(NSCoder *)coder
               sender:(id)sender
      segueIdentifier:(NSString *)segueIdentifier
{
   return [PetController initWithCoder:coder
                               petName:self.selectedPetName
                                  type:@"dog"];
}

0

Quy trình chính xác là, gọi trình khởi tạo được chỉ định mà trong trường hợp này là init với nibName,

init(tap: UITapGestureRecognizer)
{
    // Initialise the variables here


    // Call the designated init of ViewController
    super.init(nibName: nil, bundle: nil)

    // Call your Viewcontroller custom methods here

}

-1

// Bộ điều khiển chế độ xem nằm trong Main.storyboard và nó có bộ định danh

Hạng B

class func customInit(carType:String) -> BViewController 

{

let storyboard = UIStoryboard(name: "Main", bundle: nil)

let objClassB = storyboard.instantiateViewController(withIdentifier: "BViewController") as? BViewController

    print(carType)
    return objClassB!
}

Hạng A

let objB = customInit(carType:"Any String")

 navigationController?.pushViewController(objB,animated: true)
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.