การปฏิบัติที่เหมาะสมสำหรับการแบ่งคลาสย่อยของ UIView


158

ฉันกำลังทำงานกับการควบคุมอินพุตที่ใช้ UIView ที่กำหนดเองและฉันพยายามตรวจสอบให้แน่ใจว่ามีการฝึกที่เหมาะสมสำหรับการตั้งค่ามุมมอง เมื่อทำงานร่วมกับ UIViewController ก็ค่อนข้างง่ายที่จะใช้loadViewและอุปกรณ์ที่เกี่ยวข้องviewWill, viewDidวิธีการ แต่เมื่อ subclassing UIView, methosds ที่ใกล้เคียงที่สุดที่ฉันมีอยู่`awakeFromNib, และdrawRect layoutSubviews(ฉันกำลังคิดในแง่ของการตั้งค่าและการเรียกกลับที่ลดลง) ในกรณีของฉันฉันกำลังตั้งค่ากรอบและมุมมองภายในของlayoutSubviewsฉัน แต่ฉันไม่เห็นอะไรบนหน้าจอ

วิธีที่ดีที่สุดที่จะทำให้แน่ใจว่ามุมมองของฉันมีความสูงและความกว้างที่ถูกต้องที่ฉันต้องการให้มีคืออะไร (คำถามของฉันถูกนำไปใช้โดยไม่คำนึงว่าฉันกำลังใช้การชำระอัตโนมัติหรือไม่แม้ว่าอาจมีสองคำตอบ) "การปฏิบัติที่ดีที่สุด" ที่เหมาะสมคืออะไร

คำตอบ:


298

Apple ได้กำหนดวิธีการแบ่งคลาสย่อยUIViewใน doc อย่างชัดเจน

ตรวจสอบรายชื่อด้านล่างนี้โดยเฉพาะอย่างยิ่งจะดูที่และinitWithFrame: layoutSubviewsอดีตมีวัตถุประสงค์เพื่อตั้งค่าเฟรมของคุณUIViewในขณะที่หลังมีวัตถุประสงค์เพื่อตั้งค่าเฟรมและเค้าโครงของการชมย่อย

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

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

จากเอกสารของNSNibAwaking(ตอนนี้แทนที่โดยเอกสารของawakeFromNib):

ข้อความไปยังวัตถุอื่นสามารถส่งได้อย่างปลอดภัยจากภายใน awakeFromNib ซึ่งเป็นเวลาที่มั่นใจได้ว่าวัตถุทั้งหมดจะไม่ได้รับการเก็บถาวรและกำหนดค่าเริ่มต้น (แม้ว่าจะไม่ถูกกระตุ้นก็ตาม)

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

ส่งตรงจากเอกสาร :

วิธีการแทนที่

การเริ่มต้น

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

  • initWithCoder: ใช้วิธีนี้หากคุณโหลดมุมมองจากไฟล์ปลายปากกา Interface Builder และมุมมองของคุณต้องมีการกำหนดค่าเริ่มต้นเอง

  • layerClassใช้วิธีนี้เฉพาะเมื่อคุณต้องการให้มุมมองของคุณใช้เลเยอร์แกนแอนิเมชั่นอื่นสำหรับที่เก็บข้อมูลสำรอง ตัวอย่างเช่นถ้าคุณใช้ OpenGL ES เพื่อวาดภาพคุณต้องการแทนที่วิธีนี้และส่งคืนคลาส CAEAGLLayer

การวาดและการพิมพ์

  • drawRect:ใช้วิธีนี้หากมุมมองของคุณวาดเนื้อหาที่กำหนดเอง หากมุมมองของคุณไม่ได้วาดรูปแบบกำหนดเองใด ๆ หลีกเลี่ยงการเอาชนะวิธีนี้

  • drawRect:forViewPrintFormatter: ใช้วิธีนี้เฉพาะเมื่อคุณต้องการวาดเนื้อหามุมมองของคุณแตกต่างกันระหว่างการพิมพ์

ข้อ จำกัด

  • requiresConstraintBasedLayout ใช้วิธีคลาสนี้หากคลาสมุมมองของคุณต้องการข้อ จำกัด ในการทำงานอย่างถูกต้อง

  • updateConstraints ใช้วิธีนี้หากมุมมองของคุณต้องการสร้างข้อ จำกัด ที่กำหนดเองระหว่างการดูย่อยของคุณ

  • alignmentRectForFrame:, frameForAlignmentRect:ใช้วิธีการเหล่านี้เพื่อลบล้างวิธีการจัดมุมมองของคุณกับมุมมองอื่น ๆ

แบบ

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

  • layoutSubviews ใช้วิธีการนี้หากคุณต้องการการควบคุมเลย์เอาต์ของการชมย่อยของคุณอย่างแม่นยำกว่าข้อ จำกัด หรือพฤติกรรมการทำให้เป็นอัตโนมัติ

  • didAddSubview:, willRemoveSubview:ใช้วิธีการเหล่านี้ตามความจำเป็นเพื่อติดตามการเพิ่มและการลบการชมย่อย

  • willMoveToSuperview:, didMoveToSuperviewใช้วิธีการเหล่านี้เป็นสิ่งจำเป็นในการติดตามการเคลื่อนไหวของมุมมองปัจจุบันในลำดับชั้นมุมมองของคุณ

  • willMoveToWindow:, didMoveToWindowใช้วิธีการเหล่านี้เป็นสิ่งจำเป็นในการติดตามการเคลื่อนไหวของมุมมองของคุณไปยังหน้าต่างที่แตกต่างกัน

การจัดการเหตุการณ์:

  • touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent:, touchesCancelled:withEvent:ใช้วิธีการเหล่านี้ถ้าคุณจำเป็นต้องจัดการกับเหตุการณ์การสัมผัสโดยตรง (สำหรับการป้อนข้อมูลด้วยท่าทางให้ใช้ตัวจดจำลายเส้น)

  • gestureRecognizerShouldBegin: ใช้วิธีนี้หากมุมมองของคุณจัดการกิจกรรมการสัมผัสโดยตรงและอาจต้องการป้องกันตัวจดจำท่าทางสัมผัสที่แนบมาไม่ให้ทำงานเพิ่มเติม


สิ่งที่เกี่ยวกับ - (เป็นโมฆะ) setFrame: (CGRect) เฟรม
pfrank

คุณสามารถลบล้างมันได้ แต่สำหรับวัตถุประสงค์อะไร
Gabriele Petronella

เพื่อเปลี่ยนเลย์เอาต์ / การวาดภาพได้ตลอดเวลาการเปลี่ยนขนาดของเฟรมหรือตำแหน่ง
pfrank

1
เกี่ยวกับlayoutSubviewsอะไร
Gabriele Petronella

จากstackoverflow.com/questions/4000664/… "ปัญหานี้คือการดูย่อยไม่เพียง แต่สามารถเปลี่ยนขนาดได้เท่านั้น แต่พวกเขาสามารถทำให้ขนาดนั้นเปลี่ยนแปลงได้เมื่อ UIView เรียกใช้ภาพเคลื่อนไหวจะไม่เรียกใช้เค้าโครงภาพรวมในแต่ละครั้ง" ยังไม่ได้ทดสอบเป็นการส่วนตัว
pfrank

38

สิ่งนี้ยังคงสูงขึ้นใน Google ด้านล่างนี้เป็นตัวอย่างที่อัปเดตสำหรับสวิฟท์

didLoadฟังก์ชั่นช่วยให้คุณใส่รหัสเริ่มต้นที่กำหนดเองของคุณทั้งหมด ดังที่คนอื่น ๆ ได้กล่าวไว้didLoadจะถูกเรียกเมื่อมีการสร้างมุมมองโดยทางโปรแกรมinit(frame:)หรือเมื่อเครื่องมือdeserializer XIBผสานแม่แบบXIBเข้ากับมุมมองของคุณผ่านทางinit(coder:)

นอกเหนือ : layoutSubviewsและupdateConstraintsถูกเรียกหลายครั้งสำหรับมุมมองส่วนใหญ่ สิ่งนี้มีไว้สำหรับเค้าโครงและการปรับหลายพาสขั้นสูงเมื่อขอบเขตของมุมมองเปลี่ยนไป โดยส่วนตัวแล้วฉันหลีกเลี่ยงเลย์เอาต์แบบหลายรอบเมื่อเป็นไปได้เพราะเขียนรอบ CPU และทำให้ทุกอย่างปวดหัว นอกจากนี้ฉันใส่รหัสข้อ จำกัด ในการเริ่มต้นตัวเองเพราะฉันไม่ค่อยทำให้พวกเขาโมฆะ

import UIKit

class MyView: UIView {
  //-----------------------------------------------------------------------------------------------------
  //Constructors, Initializers, and UIView lifecycle
  //-----------------------------------------------------------------------------------------------------
  override init(frame: CGRect) {
      super.init(frame: frame)
      didLoad()
  }

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

  convenience init() {
    self.init(frame: CGRectZero)
  }

  func didLoad() {
    //Place your initialization code here

    //I actually create & place constraints in here, instead of in
    //updateConstraints
  }

  override func layoutSubviews() {
     super.layoutSubviews()

     //Custom manually positioning layout goes here (auto-layout pass has already run first pass)
  }

  override func updateConstraints() {
    super.updateConstraints()

    //Disable this if you are adding constraints manually
    //or you're going to have a 'bad time'
    //self.translatesAutoresizingMaskIntoConstraints = false

    //Add custom constraint code here
  }
}

คุณสามารถอธิบายได้ว่าเมื่อใด / ทำไมคุณถึงแยกรหัสข้อ จำกัด ของเลย์เอาต์ระหว่างวิธีที่เรียกจากกระบวนการเริ่มต้นและ layoutSubviews และ updateConstraints ดูเหมือนว่าพวกเขาเป็นทั้งสามตำแหน่งผู้สมัครที่เป็นไปได้ที่จะวางรหัสรูปแบบ ดังนั้นคุณจะรู้ได้อย่างไรว่าเมื่อใด / อย่างไร / ทำไมต้องแบ่งรหัสเลย์เอาต์ระหว่างสาม
Clay Ellis

3
ฉันไม่เคยใช้ updateConstraints updateConstraints นั้นดีเพราะคุณรู้ว่าลำดับชั้นการดูของคุณถูกตั้งค่าอย่างสมบูรณ์ใน init ดังนั้นคุณจึงไม่สามารถยกข้อยกเว้นได้โดยการเพิ่มข้อ จำกัด ระหว่างสองมุมมองที่ไม่ได้อยู่ในลำดับชั้น :) layoutSubviews ไม่ควรมีการแก้ไขข้อ จำกัด มันสามารถทำให้เกิดการเรียกซ้ำแบบไม่สิ้นสุดได้อย่างง่ายดายเนื่องจาก layoutSubviews ถูกเรียกว่าข้อ จำกัด นั้นเป็น 'โมฆะ' ในระหว่างการผ่านเลย์เอาต์ การตั้งค่าเลย์เอาต์แบบแมนนวล (เช่นเดียวกับในการตั้งค่าเฟรมโดยตรงซึ่งคุณไม่จำเป็นต้องทำอีกต่อไปเว้นแต่ว่าด้วยเหตุผลด้านประสิทธิภาพ) ไปที่ layoutSubviews โดยส่วนตัวแล้วฉันสร้างการ จำกัด ใน init
seo

สำหรับโค้ดการแสดงผลที่กำหนดเองเราควรแทนที่drawเมธอดหรือไม่
Petrus Theron

14

มีบทสรุปที่ดีในเอกสารประกอบของ Apple และครอบคลุมอยู่ในหลักสูตรฟรีของStanfordบน iTunes ฉันแสดงรุ่น TL ของฉันที่นี่:

หากชั้นเรียนของคุณส่วนใหญ่ประกอบด้วยหัวข้อย่อยสถานที่ที่เหมาะสมในการจัดสรรเป็นinitวิธีการ สำหรับมุมมองมีสองinitวิธีที่แตกต่างกันที่อาจถูกเรียกขึ้นอยู่กับว่ามุมมองของคุณถูกสร้างอินสแตนซ์จากโค้ดหรือจากปลายปากกา / กระดานเรื่องราว สิ่งที่ผมทำคือการเขียนของตัวเองsetupวิธีการแล้วโทรได้จากทั้งสองinitWithFrame:และinitWithCoder:วิธีการ

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

แทนที่เฉพาะในlayoutSubViewsกรณีที่คุณต้องการทำอะไรบางอย่างเช่นเพิ่มหรือลบมุมมองย่อยขึ้นอยู่กับว่าคุณอยู่ในแนวตั้งหรือแนวนอน มิฉะนั้นคุณควรทิ้งไว้คนเดียว


ฉันใช้คำตอบของคุณเพื่อเปลี่ยนมุมมอง (ซึ่งเป็นเฟรมย่อยของวิวเวอร์ awakeFromNib) ซึ่งlayoutSubViewsทำงานได้ดี
เครื่องบิน

1

layoutSubviews มีวัตถุประสงค์เพื่อตั้งค่าเฟรมในมุมมองชายน์ไม่ใช่ในมุมมองตัวเอง

สำหรับUIViewConstructor ที่กำหนดโดยทั่วไปแล้วinitWithFrame:(CGRect)frameและคุณควรตั้งค่าเฟรมที่นั่น (หรือในinitWithCoder:) ซึ่งอาจไม่ผ่านค่าเฟรม นอกจากนี้คุณยังสามารถสร้างนวกรรมิกที่แตกต่างกันและตั้งค่าเฟรมที่นั่น


คุณสามารถเอารายละเอียดเพิ่มเติมไปได้ไหม ฉันไม่ทราบค่าเฉลี่ยของคุณจะกำหนดเฟรมย่อยของมุมมองอย่างไร มุมมองคือawakeFromNib
เครื่องบิน

กรอไปข้างหน้าถึงปี 2559 คุณอาจไม่ควรกำหนดเฟรมเลยและใช้การชำระอัตโนมัติ (ข้อ จำกัด ) หากมุมมองมาจาก XIB (หรือกระดานเรื่องราว) มุมมองย่อยควรได้รับการตั้งค่าแล้ว
proxi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.