กำหนดค่าเริ่มต้นสำหรับ UIViewController ใน Swift พร้อมการตั้งค่าอินเทอร์เฟซในสตอรี่บอร์ด


89

ฉันมีปัญหาในการเขียน init ที่กำหนดเองสำหรับคลาสย่อยของ UIViewController โดยพื้นฐานแล้วฉันต้องการส่งการอ้างอิงผ่านเมธอด init สำหรับ viewController แทนที่จะตั้งค่าคุณสมบัติโดยตรงเช่น viewControllerB.property = value

ดังนั้นฉันจึงสร้าง init ที่กำหนดเองสำหรับ viewController ของฉันและเรียกใช้ super ที่กำหนด

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

อินเทอร์เฟซตัวควบคุมมุมมองอยู่ในสตอรีบอร์ดฉันยังทำให้อินเทอร์เฟซสำหรับคลาสที่กำหนดเองเป็นตัวควบคุมมุมมองของฉัน และ Swift จำเป็นต้องเรียกวิธีการเริ่มต้นนี้แม้ว่าคุณจะไม่ได้ทำอะไรภายในวิธีนี้ก็ตาม ไม่งั้นคอมเพลนจะบ่น ...

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

ปัญหาคือเมื่อฉันพยายามเรียกใช้ init ที่กำหนดเองของฉันโดยMyViewController(meme: meme)ไม่ได้ init คุณสมบัติใน viewController เลย ...

ฉันกำลังพยายามดีบักฉันพบใน viewController ของฉันinit(coder aDecoder: NSCoder)เรียกก่อนจากนั้นจึงเรียก init ที่กำหนดเองของฉันในภายหลัง อย่างไรก็ตามวิธีการ init ทั้งสองนี้ส่งคืนselfที่อยู่หน่วยความจำที่แตกต่างกัน

ฉันสงสัยว่ามีบางอย่างผิดปกติกับ init สำหรับ viewController ของฉันและมันจะกลับมาselfพร้อมกับinit?(coder aDecoder: NSCoder)ซึ่งไม่มีการนำไปใช้งานเสมอ

ไม่มีใครรู้วิธีสร้างกำหนดเองสำหรับ viewController ของคุณอย่างถูกต้อง? หมายเหตุ: อินเทอร์เฟซ viewController ของฉันถูกตั้งค่าในสตอรี่บอร์ด

นี่คือรหัส viewController ของฉัน:

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)
    }

}

คุณได้รับวิธีแก้ปัญหานี้หรือไม่?
user481610

คำตอบ:


50

ตามที่ระบุไว้ในคำตอบข้างต้นคุณไม่สามารถใช้ทั้งวิธีการเริ่มต้นและกระดานเรื่องราวที่กำหนดเองได้

แต่คุณยังคงสามารถใช้วิธีการคงที่เพื่อสร้างอินสแตนซ์ViewControllerจากสตอรีบอร์ดและตั้งค่าเพิ่มเติมได้

จะมีลักษณะดังนี้:

class MemeDetailVC : UIViewController {
    
    var meme : Meme!
    
    static func makeMemeDetailVC(meme: Meme) -> MemeDetailVC {
        let newViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "IdentifierOfYouViewController") as! MemeDetailVC
        
        newViewController.meme = meme
        
        return newViewController
    }
}

อย่าลืมระบุ IdentifierOfYouViewController เป็นตัวระบุตัวควบคุมมุมมองในสตอรีบอร์ดของคุณ คุณอาจต้องเปลี่ยนชื่อสตอรี่บอร์ดในโค้ดด้านบน


หลังจากเริ่มต้น MemeDetailVC คุณสมบัติ "meme" จะมีอยู่ จากนั้นการฉีดพึ่งพาจะเข้ามากำหนดค่าให้กับ "meme" ขณะนี้ยังไม่ได้เรียก loadView () และ viewDidLoad ทั้งสองวิธีจะถูกเรียกหลังจาก MemeDetailVC add / push เพื่อดูลำดับชั้น
Jim Yu

คำตอบที่ดีที่สุดที่นี่!
PascalS

ยังคงต้องใช้วิธีการ init และคอมไพเลอร์จะบอกว่าคุณสมบัติที่เก็บ meme ยังไม่ได้เริ่มต้น
Surendra Kumar

27

คุณไม่สามารถใช้โปรแกรมเริ่มต้นที่กำหนดเองได้เมื่อคุณเริ่มต้นจาก Storyboard โดยใช้init?(coder aDecoder: NSCoder)วิธีที่ Apple ออกแบบสตอรี่บอร์ดเพื่อเริ่มต้นคอนโทรลเลอร์ อย่างไรก็ตามมีหลายวิธีในการส่งข้อมูลไปยังไฟล์UIViewController.

ตัวควบคุมมุมมองของคุณมีชื่อdetailอยู่ในนั้นดังนั้นฉันคิดว่าคุณไปที่นั่นจากคอนโทรลเลอร์อื่น ในกรณีนี้คุณสามารถใช้prepareForSegueวิธีการส่งข้อมูลไปยังรายละเอียด (นี่คือ Swift 3):

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

ฉันเพิ่งใช้คุณสมบัติประเภทหนึ่งStringแทนที่จะใช้Memeเพื่อการทดสอบ นอกจากนี้ตรวจสอบให้แน่ใจว่าคุณส่งตัวระบุการทำต่อที่ถูกต้อง ( "identifier"เป็นเพียงตัวยึดตำแหน่ง)


1
สวัสดี แต่เราจะกำจัดการระบุเป็นตัวเลือกได้อย่างไรและเราก็ไม่อยากผิดพลาดที่จะไม่กำหนดมัน เราแค่ต้องการให้การพึ่งพาที่เข้มงวดมีความหมายสำหรับตัวควบคุมตั้งแต่แรก
Amber K

1
@AmberK คุณอาจต้องการดูการเขียนโปรแกรมอินเทอร์เฟซของคุณแทนที่จะใช้ Interface Builder
Caleb Kleveter

โอเคฉันกำลังมองหาโครงการ kickstarter ios สำหรับการอ้างอิงเช่นกันยังไม่สามารถบอกได้ว่าพวกเขาส่งโมเดลมุมมองของพวกเขาอย่างไร
Amber K

23

ดังที่ @Caleb Kleveter ได้ชี้ให้เห็นเราไม่สามารถใช้โปรแกรมเริ่มต้นที่กำหนดเองได้ในขณะที่เริ่มต้นจากสตอรี่บอร์ด

แต่เราสามารถแก้ปัญหาได้โดยใช้วิธีการโรงงาน / คลาสซึ่งสร้างอินสแตนซ์ออบเจ็กต์ตัวควบคุมมุมมองจาก Storyboard และวัตถุคอนโทรลเลอร์มุมมองกลับ ฉันคิดว่านี่เป็นวิธีที่ค่อนข้างดี

หมายเหตุ:นี่ไม่ใช่คำตอบที่แน่นอนสำหรับคำถาม แต่เป็นวิธีแก้ปัญหาในการแก้ปัญหา

สร้างเมธอดคลาสในคลาส MemeDetailVC ดังนี้:

// 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
}

การใช้งาน:

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

5
แต่ยังคงมีข้อเสียอยู่คุณสมบัติจะต้องเป็นทางเลือก
Amber K

เมื่อใช้class func init (meme: Meme) -> MemeDetailVCXcode 10.2 จะสับสนและทำให้เกิดข้อผิดพลาดIncorrect argument label in call (have 'meme:', expected 'coder:')
Samuël

21

วิธีหนึ่งที่ฉันทำคือการเริ่มต้นใช้งานสะดวก

class MemeDetailVC : UIViewController {

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

จากนั้นคุณเริ่มต้น MemeDetailVC ของคุณด้วย let memeDetailVC = MemeDetailVC(theMeme)

เอกสารของ Apple เกี่ยวกับ initializersนั้นค่อนข้างดี แต่สิ่งที่ฉันชอบคือRay Wenderlich: Initialization in Depth tutorial series ซึ่งควรให้คำอธิบาย / ตัวอย่างมากมายเกี่ยวกับตัวเลือกการเริ่มต้นต่างๆของคุณและวิธีที่ "เหมาะสม" ในการทำสิ่งต่างๆ


แก้ไข : ในขณะที่คุณสามารถใช้ตัวเริ่มต้นอำนวยความสะดวกบนตัวควบคุมมุมมองที่กำหนดเองได้ทุกคนมีความถูกต้องในการระบุว่าคุณไม่สามารถใช้ตัวเริ่มต้นที่กำหนดเองได้เมื่อเริ่มต้นจากกระดานเรื่องราวหรือผ่านทางกระดานเรื่องราว

หากอินเทอร์เฟซของคุณได้รับการตั้งค่าในกระดานเรื่องราวและคุณกำลังสร้างตัวควบคุมโดยใช้โปรแกรมอย่างสมบูรณ์ตัวเริ่มต้นความสะดวกอาจเป็นวิธีที่ง่ายที่สุดในการทำสิ่งที่คุณพยายามทำเนื่องจากคุณไม่ต้องจัดการกับการเริ่มต้นที่จำเป็น NSCoder (ซึ่งฉันยังไม่เข้าใจจริงๆ)

หากคุณได้รับตัวควบคุมมุมมองของคุณผ่านกระดานเรื่องราวคุณจะต้องทำตามคำตอบของ @Caleb Kleveterและส่งตัวควบคุมมุมมองไปยังคลาสย่อยที่คุณต้องการจากนั้นตั้งค่าคุณสมบัติด้วยตนเอง


1
ฉันไม่เข้าใจถ้าอินเทอร์เฟซของฉันถูกตั้งค่าในกระดานเรื่องราวและฉันกำลังสร้างมันโดยใช้โปรแกรมฉันต้องเรียกวิธีการสร้างอินสแตนซ์โดยใช้สตอรี่บอร์ดเพื่อโหลดอินเทอร์เฟซใช่ไหม จะconvenienceช่วยได้อย่างไรที่นี่
Amber K

ไม่สามารถเริ่มต้นคลาสอย่างรวดเร็วโดยการเรียกinitใช้ฟังก์ชันอื่นของคลาส (โครงสร้างสามารถทำได้) ดังนั้นในการโทรself.init()คุณต้องทำเครื่องหมายinit(meme: Meme)ว่าเป็นตัวconvenienceเริ่มต้น มิฉะนั้นคุณจะต้องตั้งค่าคุณสมบัติที่จำเป็นทั้งหมดของUIViewControllerตัวเริ่มต้นด้วยตัวเองและฉันไม่แน่ใจว่าคุณสมบัติทั้งหมดนั้นคืออะไร
Ponyboy47

นี่ไม่ใช่คลาสย่อยของ UIViewController เนื่องจากคุณกำลังเรียก self.init () ไม่ใช่ super.init () คุณยังคงสามารถเริ่มต้น MemeDetailVC โดยใช้ค่าเริ่มต้นเช่นMemeDetail()และในกรณีนี้รหัสจะผิดพลาด
อาลี

ยังถือว่าเป็นคลาสย่อยเนื่องจาก self.init () จะต้องเรียก super.init () ในการนำไปใช้งานหรือบางที self.init () จะสืบทอดโดยตรงจากผู้ปกครองซึ่งในกรณีนี้จะมีฟังก์ชันเทียบเท่ากัน
Ponyboy47

6

เดิมมีคำตอบสองสามคำตอบซึ่งได้รับการโหวตจากวัวและถูกลบแม้ว่าจะเป็นคำตอบที่ถูกต้องก็ตาม คำตอบคือคุณทำไม่ได้

เมื่อทำงานจากนิยามสตอรีบอร์ดอินสแตนซ์ตัวควบคุมมุมมองของคุณจะถูกเก็บถาวรทั้งหมด ดังนั้นในการเริ่มต้นจึงจำเป็นต้องinit?(coder...ใช้ coderเป็นที่ที่ทุกข้อมูลการตั้งค่า / มุมมองมาจาก

ดังนั้นในกรณีนี้จึงไม่สามารถเรียกใช้ฟังก์ชัน init อื่น ๆ ด้วยพารามิเตอร์ที่กำหนดเองได้ ควรตั้งค่าเป็นคุณสมบัติเมื่อเตรียมการต่อหรือคุณสามารถทิ้ง segues และโหลดอินสแตนซ์โดยตรงจากกระดานเรื่องราวและกำหนดค่าได้ (โดยทั่วไปจะเป็นรูปแบบโรงงานโดยใช้สตอรี่บอร์ด)

ในทุกกรณีคุณใช้ฟังก์ชัน init ที่จำเป็นของ SDK และส่งผ่านพารามิเตอร์เพิ่มเติมในภายหลัง


4

สวิฟต์ 5

คุณสามารถเขียน initializer แบบกำหนดเองได้ดังนี้ ->

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

UIViewControllerคลาสเป็นไปตามNSCodingโปรโตคอลซึ่งกำหนดเป็น:

public protocol NSCoding {

   public func encode(with aCoder: NSCoder)

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

ดังนั้นจึงUIViewControllerมีตัวเริ่มต้นที่กำหนดสองตัวinit?(coder aDecoder: NSCoder)และinit(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?).

Storyborad โทรinit?(coder aDecoder: NSCoder)ไปที่ init โดยตรงUIViewControllerและUIViewไม่มีที่ว่างให้คุณส่งผ่านพารามิเตอร์

วิธีแก้ปัญหาที่ยุ่งยากอย่างหนึ่งคือการใช้แคชชั่วคราว:

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
}


1

โฟลว์ที่ถูกต้องคือเรียก initializer ที่กำหนดซึ่งในกรณีนี้คือ init ด้วย 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

}

0

Disclaimer: ผมไม่สนับสนุนการนี้และยังไม่ได้ทดสอบอย่างละเอียดความยืดหยุ่นของมัน แต่มันเป็นศักยภาพการแก้ปัญหาที่ผมค้นพบในขณะที่เล่นรอบ

ในทางเทคนิคแล้วการเริ่มต้นแบบกำหนดเองสามารถทำได้ในขณะที่ยังคงรักษาอินเทอร์เฟซที่กำหนดค่าสตอรี่บอร์ดไว้โดยการเริ่มต้นตัวควบคุมมุมมองสองครั้ง: ครั้งแรกผ่านทางกำหนดเองของคุณinitและครั้งที่สองภายในloadView()ที่คุณใช้มุมมองจากสตอรีบอร์ด

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)
  }
}

ตอนนี้ที่อื่นในแอปของคุณคุณสามารถเรียกโปรแกรมเริ่มต้นที่กำหนดเองได้เช่นCustomViewController(someParameter: foo)และยังคงได้รับการกำหนดค่ามุมมองจากสตอรีบอร์ด

ฉันไม่คิดว่านี่เป็นทางออกที่ดีด้วยเหตุผลหลายประการ:

  • การเริ่มต้นอ็อบเจ็กต์ซ้ำกันรวมถึงคุณสมบัติก่อนการเริ่มต้น
  • พารามิเตอร์ที่ส่งผ่านไปยังกำหนดเองinitต้องถูกเก็บไว้เป็นคุณสมบัติทางเลือก
  • เพิ่มแผ่นสำเร็จรูปซึ่งต้องได้รับการบำรุงรักษาเนื่องจากร้านค้า / คุณสมบัติมีการเปลี่ยนแปลง

บางทีคุณอาจยอมรับการแลกเปลี่ยนเหล่านี้ได้แต่ใช้ความเสี่ยงของคุณเองแต่ใช้ที่มีความเสี่ยงของคุณเอง


0

แม้ว่าตอนนี้เราสามารถทำการ init แบบกำหนดเองสำหรับคอนโทรลเลอร์เริ่มต้นในสตอรี่บอร์ดโดยใช้ instantiateInitialViewController(creator:)และสำหรับเซ็กส์รวมถึงความสัมพันธ์และการแสดง

ความสามารถนี้ถูกเพิ่มใน Xcode 11 และต่อไปนี้เป็นข้อความที่ตัดตอนมาจากบันทึกย่อประจำรุ่น Xcode 11 :

วิธีการควบคุมมุมมองที่มีคำอธิบายประกอบกับ@IBSegueActionแอตทริบิวต์ใหม่สามารถใช้เพื่อสร้างตัวควบคุมมุมมองปลายทางของผู้ติดตามในโค้ดโดยใช้ค่าเริ่มต้นที่กำหนดเองพร้อมค่าที่ต้องการ สิ่งนี้ทำให้สามารถใช้ตัวควบคุมมุมมองที่มีข้อกำหนดการเริ่มต้นที่ไม่ใช่ทางเลือกในสตอรีบอร์ด สร้างการเชื่อมต่อจากการทำต่อไปยัง@IBSegueActionวิธีการบนตัวควบคุมมุมมองต้นทาง บน OS รุ่นใหม่ที่สนับสนุน Segue การดำเนินการวิธีการที่จะถูกเรียกว่าคุ้มค่าและผลตอบแทนก็จะเป็นของวัตถุทำต่อผ่านไปdestinationViewController prepareForSegue:sender:หลายวิธีการอาจจะกำหนดในตัวควบคุมมุมมองแหล่งเดียวซึ่งสามารถบรรเทาความจำเป็นที่จะต้องตรวจสอบสตริงระบุทำต่อใน@IBSegueAction prepareForSegue:sender:(47091566)

IBSegueActionวิธีการใช้เวลาถึงสามพารามิเตอร์: ก coder ผู้ส่งและระบุที่ทำต่อของ จำเป็นต้องมีพารามิเตอร์แรกและพารามิเตอร์อื่น ๆ สามารถละเว้นจากลายเซ็นของวิธีการของคุณได้หากต้องการ NSCoderจะต้องมีการส่งผ่านไปยัง initializer มุมมองควบคุมปลายทางเพื่อให้แน่ใจว่ามันมีค่าที่กำหนดเองการกำหนดค่าในสตอรี่บอร์ด วิธีนี้จะส่งคืนตัวควบคุมมุมมองที่ตรงกับชนิดตัวควบคุมปลายทางที่กำหนดไว้ในกระดานเรื่องราวหรือnilเพื่อทำให้ตัวควบคุมปลายทางเริ่มต้นด้วยinit(coder:)วิธีมาตรฐาน ถ้าคุณรู้ว่าคุณไม่จำเป็นต้องกลับมาnilคืนประเภทการส่งคืนอาจเป็นแบบไม่บังคับ

ใน Swift ให้เพิ่ม@IBSegueActionแอตทริบิวต์:

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

ใน Objective-C ให้เพิ่มIBSegueActionหน้าประเภทการส่งคืน:

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

-1

// View controller อยู่ใน Main.storyboard และมีชุดตัวระบุ

คลาส 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!
}

คลาส A

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

 navigationController?.pushViewController(objB,animated: true)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.