บทความและข่าวสาร | Seven Peaks Insights

UIKit และการออกแบบ Decorator Pattern

วิธีการใช้ UIkit ใน SwiftUI

Andrei-Photo
 

เนื้อหาที่น่าสนใจในซีรีส์ “Design patterns สำหรับ iOS” นี้ของ Andrey Soloviev ชายผู้มีตำแหน่งเป็นนักพัฒนา iOS อาวุโสของเรา เขาจะมาอธิบายถึง design pattern ที่ครอบคลุมทุกพื้นฐานที่เกี่ยวข้องทั้งหมดและแสดงวิธีใช้ UIkit ใน SwiftUI โดยเขาจะชี้ให้เห็นถึงแนวทางการปฏิบัติที่ดีที่สุดของ Swift ให้คุณและคนที่สนใจในเครื่องมือนี้ได้รู้กัน

Design pattern เป็นรูปแบบการแก้ไขปัญหาสำหรับปัญหาที่พบได้บ่อย ซึ่งจะทำให้คุณมีอิสระและรู้สึกมั่นใจมากขึ้นในการเลือกวิธีแก้ไขปัญหาที่มีความเฉพาะเจาะจง

 

การนำเสนอในครั้งนี้ผมเลือกที่จะเอาตัวอย่างง่ายๆ มาแสดงให้พวกคุณได้ดูกัน เนื่องจากผมต้องการเน้นไปที่การออกแบบ decorator pattern รวมถึงเราจะใช้ UIView ด้วยวิธีที่ผิดหลักการแต่สามารถทำให้การใช้งานสิ่งนี้ง่ายขึ้นได้อย่างไร

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

import UIKit
class HyperlinkDecorator {
var url: URL?
var label: UILabel
required init(label: UILabel) {
self.label = label
label.isUserInteractionEnabled = true label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(hyperlinkedLabelTapped)))
}
@objc private func hyperlinkedLabelTapped() {
if let url = url, UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url)
}
}
}

 

คุณจะเห็นได้ว่าการนำไปใช้งานจริงนั้นตรงไปตรงมาไม่ได้มีอะไรซับซ้อน เพียงต้องใช้ UILabel กับเพิ่ม UITapGestureRecognizer เข้าไป และเมื่อแตะมันก็จะเปิด URL ขึ้นมา ซึ่งเราเก็บ URL ไว้หรือไม่ก็ได้ เพื่อให้สามารถเปลี่ยนแปลงแก้ไขได้ตลอดเวลา แต่ขอให้คุณดูให้แน่ใจว่าได้กำหนด HyperlinkDecorator เป็น class ไม่ใช่โครงสร้าง นั่นก็เพราะว่า property นี้เป็นประเภท reference type

อย่างเช่นในตัวอย่างนี้ เราไม่จำเป็นต้องอ้างอิงถึง UILabel ขอแค่ปล่อยมันไว้ตรงนั้น แต่ถ้าตอนนี้หากเราต้องการ decorator ของเรา ก็จำเป็นต้องสร้างและจัดเก็บมันไว้ในที่ใดที่หนึ่ง ในส่วน UIGestureRecognizer นั้นจะไม่สามารถเก็บ target ไว้ได้ ดังนั้นถ้าเราไม่เก็บการอ้างอิงเพื่อเชื่อมไปยัง HyperlinkDecorator มันก็จะถูก deallocate ทันที

class HyperlinkViewController: UIViewController {
@IBOutlet weak var hyperLabel: UILabel!
var hyperlinkDecorator: HyperlinkDecorator?
override func viewDidLoad() {
super.viewDidLoad()
hyperlinkDecorator = HyperlinkDecorator(label: hyperLabel)
hyperlinkDecorator?.url = URL(string: “https://medium.com”)
}
}

 

ตัวผมเองไม่ชอบที่จะเก็บ reference สำหรับ object ที่ไม่ได้ใช้งานจริงๆ เพราะถ้าเรามีหลาย decorator เราก็จะต้องสร้าง property เพิ่มเติมสำหรับแต่ละ decorator และนั่นส่งผลให้โค้ดอ่านและทำการบำรุงรักษาได้ยากขึ้นนั่นเอง

นอกจากนี้ เรายังสามารถสร้าง array หรือ set ของ object ดังกล่าวและจัดเก็บทั้งหมดไว้ที่นั่น ดังนั้น object เหล่านั้นจะถูก deallowcate พร้อมกับ HyperlinkViewController

class HyperlinkViewController: UIViewController {
@IBOutlet weak var hyperLabel: UILabel!
var decorators = [Any]()
override func viewDidLoad() {
super.viewDidLoad()
let hyperlinkDecorator = HyperlinkDecorator(label: hyperLabel)
hyperlinkDecorator.url = URL(string: “https://medium.com”)
decorators.append(hyperlinkDecorator)
}
}

 

“Decorator เป็นรูปแบบการแก้ไขปัญหาเชิงโครงสร้างที่ให้คุณเพิ่ม behaviorใหม่ๆ กับ object ได้ โดยการวาง object เหล่านี้ไว้ใน object พิเศษที่บรรจุมี ehavior ดังกล่าวเอาไว้” – Refactoring.Guru

   

มันอาจจะดูดีกว่าแต่ผมก็ยังรู้สึกไม่ชอบเท่าไรนัก แต่นั่นก็คือแนวทางที่พาผมมาถึงวิธีแก้ปัญหาถัดไปที่ผมยังคงใช้เพื่อกำจัดการอ้างอิงดังกล่าวด้วยการทำงานกับ component จำนวนมากของ UIKit

เราทำการย่อย HyperlinkDecorator จาก UIView ด้วยการทำให้มันมีขนาดเป็นศูนย์และเพิ่มเป็น subview ของ UILabel ดังนั้นเจ้าสิ่งนี้จะถูกใช้หรือเลิกใช้งานไปพร้อมกับ UILabel

นอกจากนี้ เรายังตั้งค่า reference ที่ไม่รัดกุมให้กับ UILabel เพื่อหลีกเลี่ยงการเกิดวงจรซ้ำๆ และการรั่วไหลของหน่วยความจำ

import UIKit

class HyperlinkDecorator: UIView {
var url: URL?
weak var label: UILabel?

required init(label: UILabel) {
self.label = label
super.init(frame: .zero)
label.isUserInteractionEnabled = true
label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(hyperlinkedLabelTapped)))
label.addSubview(self)
}

// It’s required for UIView subclasses
required init?(coder: NSCoder) {
fatalError(“init(coder:) has not been implemented”)
}

@objc private func hyperlinkedLabelTapped() {
if let url = url, UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
}

 

การใช้งานยังคงเหมือนเดิม แต่เราไม่ต้องเก็บ reference อีกต่อไป

นั่นดูเหมือนจะโอเค แต่ที่จริงแล้วเราสามารถทำมันให้ออกมาดีได้มากกว่านี้อีก มาเริ่มสร้างส่วนขยายสำหรับ UILabel ที่จะช่วยซ่อนการใช้งานนี้และให้ URL property ที่สะดวกกว่าให้แก่เรา

extension UILabel {
var url: URL? {
get {
subview(type: HyperlinkDecorator.self)?.url
}
set {
let decorator = subview(type: HyperlinkDecorator.self) ?? HyperlinkDecorator(label: self)
decorator.url = newValue
}
}
}

extension UILabel {
func subview(type: V.Type) -> V? {
return subviews.first(where: { $0 is V }) as? V
}
}

 

มี subview(type:) ของฟังก์ชันทั่วไปที่ใช้ค้นหา subview ประเภทพิเศษ โดยผมสามารถใช้แท็กในส่วนนี้ได้ แต่ผมกลับชอบเวอร์ชันนี้มากกว่า เนื่องจากโค้ดดูสะอาดกว่า และยังไม่ต้องกังวลเกี่ยวกับการกำหนดค่าแท็กที่ไม่ซ้ำใคร และนั่นคือวิธีการที่เราสามารถใช้ได้ในตอนนี้

class HyperlinkViewController: UIViewController {
@IBOutlet weak var hyperLabel: UILabel!

override func viewDidLoad() {
super.viewDidLoad()
hyperLabel.url = URL(string: “https://medium.com”)
}
}

 

ตอนนี้มันดูดีขึ้นมาก และจริงๆ แล้วผมก็คิดที่จะหยุดอยู่แค่ตรงนี้ แต่ก็ยังสามารถ refactor ส่วนขยายของ UILabel และทำให้เพิ่มคุณสมบัติใหม่และ decorator ใหม่ได้ง่ายขึ้น

นี่คือสิ่งที่ผมกำลังจะทำ:

  • ย้าย subview(type:) ไปยังส่วนขยาย UIView
  • เพิ่มโปรโตคอล ViewDecorator
  • เพิ่มฟังก์ชันทั่วไปอย่าง viewDecorator(type:) ลงในส่วนขยาย UIView ที่ต้องการจะค้นหาและสร้าง decorator ขึ้นมาใหม่หากหาแล้วไม่พบ
  • สอดคล้องกับ HyperlinkDecorator รวมถึงโปรโตคอล ViewDecorator
  • ส่วนขยาย UILabel

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

โดยการย้ายฟังก์ชัน subview(type:) ไปยัง UIView:

import UIKit

extension UIView {
func subview(type: V.Type = V.self ) -> V? {
return subviews.first(where: { $0 is V }) as? V
}
}

 

View decorator ควรเป็น subclass ของ UIView และควรใช้ object ทั่วไปของ UIView หรือ subclass ของมัน นั่นก็เพราะว่า class ที่แน่นอนของ object ทั่วไปนี้จะถูกกำหนดในแต่ละมุมมองของ decorator

import UIKit

protocol ViewDecorator: UIView {
associatedtype View: UIView
init(object: View)
}

 

ตอนนี้มาเริ่มต้นย้ายโค้ดของการค้นหาและสร้าง decorator ใหม่จากส่วนขยาย UILabel ไปยังฟังก์ชันทั่วไปที่แยกกันจากภายในส่วนขยายของ UIView:

import UIKit

extension UIView {
func viewDecorator(type: V.Type = V.self) -> V {
return subview(type: V.self) ?? V(object: self as! V.View)
}
}

 

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

การทำให้ HyperlinkDecorator สอดคล้องกับ ViewDecorator นั้นง่ายมาก เพียงเพิ่มการยืนยันโปรโตคอลและแทนที่ init(label:) ด้วย init(object:)

นี่คือโค้ดที่สมบูรณ์สำหรับ HyperlinkDecorator

 

import UIKit

class HyperlinkDecorator: UIView, ViewDecorator {
var url: URL?
weak var label: UILabel?

required init(object label: UILabel) {
self.label = label
super.init(frame: .zero)
label.isUserInteractionEnabled = true
label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(hyperlinkedLabelTapped)))
label.addSubview(self)
}

// It’s required to have for UIView subclasses
required init?(coder: NSCoder) {
fatalError(“init(coder:) has not been implemented”)
}

@objc private func hyperlinkedLabelTapped() {
if let url = url, UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
}

 

และในที่สุดก็มาถึงการอัปเดตส่วนขยายของ UILabel ซึ่งเป็นหัวใจสำคัญของขั้นตอนเหล่านี้ทั้งหมด

แค่นั้นแหละ นั่นคือทั้งหมด คุณสามารถค้นหาโครงการตัวอย่างที่สมบูรณ์แบบได้ที่นี่

import UIKit

extension UILabel {
var url: URL? {
get { viewDecorator(type: HyperlinkDecorator.self).url }
set { viewDecorator(type: HyperlinkDecorator.self).url = newValue }
}
}

คุณต้องการนักพัฒนา iOS ระดับอาวุโสเพื่อออกแบบและพัฒนาแอปฯ ของคุณหรือไม่?
ติดต่อเราเพื่อคุยกันว่าเราจะช่วยเหลือคุณอย่างไรได้บ้าง
พูดคุยกับเรา