AppleのCombineフレームワークの基本的な内容について
!Combineೖ2020.6.11 iOSLT / @shtnkgm1
View Slide
Combineͱ• Swift͚Framework• WWDC2019Ͱൃද• iOS 13.0+• ඇಉظॲཧΛѻ͏ͷ2
UIKitͷඇಉظॲཧ• Taregt/ActionʢTimer, @IBAction ...ʣ• NotificationCenter• URLSession• KVOʢKey-value observingʣ• Callback࣮ʢDelegate, Closureʣ3
ඇಉظॲཧʹ͓͚Δ՝• ωετͨ͠Closure Callbackࠈ• Delegateॻ͘ͷಡΉͷΊΜͲ͍͘͞ࠈ• ඇಉظΠϕϯτͷछྨ࣮͍Ζ͍ΖɺྲྀΕ͕Θ͔Βͳ͘ͳΔ4
Combine͜ΕΒͷඇಉظॲཧΛҰݩతɺએݴతʹѻ͏ͨΊͷAPI5
ओͳAPI• Publisher• Subscriber• Operator6
Publisher• Τϥʔ͕ͲͷΑ͏ʹൃੜ͢Δ͔Λఆٛ• SubscriberͷొΛՄೳʹ͢Δʢߪಡʣ• ࣮ܕʢstructʣ7
Publisherͷఆٛprotocol Publisher {associatedtype Outputassociatedtype Failure: Error// Subscriberͷొfunc subscribe(_ subscriber: S)where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input}8
NotificationCenterͰͷPublisher࣮ྫextension NotificationCenter {struct Publisher: Combine.Publisher {typealias Output = Notifiationtypealias Failure = Never // Τϥʔͳ͍߹Neverܕinit(center: NotificationCenter,name: Notification.Name,object: Any? = nil)}}9
ฦ͞ͳ͍ͷͬͯVoid͡Όͳ͍ͷʁ10
Neverܕpublic enum Never {}• ࣮caseͳ͠enum• ͭ·ΓΛͭ͘Εͳ͍͜ͱΛදݱ11
Voidܕpublic typealias Void = ()• ࣮ۭͷλϓϧͷΤΠϦΞε• ͭ·ΓۭͷλϓϧΛฦ͍ͯ͠Δ12
fatalErrorͷΓNeverܕclass ViewController: UIViewControllerinit() {...}required init?(coder aDecoder: NSCoder) {// NeverܕΛฦ͢͜ͱͰϓϩάϥϜऴྃfatalError("init(coder:) has not been implemented")}func hoge() -> Int {// ΓͷܕݕࠪߦΘΕͳ͍ͷͰ// ͜ΕͰίϯύΠϧ͕௨ΔfatalError()}}13
Subscriber• ͱྃΛड͚औΔ• ࢀরܕʢclassʣ14
Subscriberͷఆٛprotocol Subscriber {associatedtype Inputassociatedtype Failure : Error// SubscriptionΛड͚औΔfunc receive(subscription: Subscription)// ೖྗΛड͚औΔfunc receive(_ input: Self.Input) -> Subscribers.Demand// ྃΛड͚औΔʢ༗ݶͷ߹ʣfunc receive(completion: Subscribers.Completion)}15
Subscriberͷ࣮ྫʢAssignʣࢦఆ͞ΕͨΩʔύεʹΛηοτ͢ΔSubscriber// Subscribersͨͩͷnamespaceͱͯ͠ͷenum// AssignͷଞʹɺDemand, Completion, Sink͕͋Δextension Subscribers {class Assign: Subscriber, Cancellable {typealias Faiure = Neverinit(object: Root, keyPath: ReferenceWritableKeyPath)}}16
Operator• Publisherϓϩτίϧʹ४ڌ• ͕มԽ͢Δ;Δ·͍Λఆٛ• ܕʢstructʣ17
Operatorͷ࣮ྫʢMapʣ// ͜Εnamespace// MapҎ֎ʹSequence, Catch, ReceiveOn, SubscribeOn...ͳͲΊͬͪΌ͋Δextension Publishers {struct Map where Upstream : Publisher {typealias Failure = Upstream.Failurelet upstream: Upstreamlet transform: (Upstream.Output) -> Output}}18
OperatorͰͰ͖Δ͜ͱͷҰྫ• ؔతͳͷม• Ϧετૢ࡞• ΤϥʔϋϯυϦϯά• εϨουɺΩϡʔΠϯάॲཧʢαϒεϨουॲཧʣ• εέδϡʔϦϯάʢλΠϚʔॲཧʣ19
Publisher, Subscriber, OperatorΛͬͯΈΔຐ๏ֶߍͱֶclass Wizard {var grade: Intinit(grade: Int) {self.grade = grade}}let merlin = Wizard(grade: 5)20
Publisher, Subscriber, OperatorΛͬͯΈΔlet graduationPublisher =NotificationCenter.Publisher(center: .default, name: .graduated, object: merlin)let gradeSubscriber = Subscribers.Assign(object: merlin, keyPath: \.grade)let converter = Publishers.Map(upstream: graduationPublisher) { note inreturn note.userInfo?["NewGrade"] as? Int ?? 0}converter.subscribe(gradeSubscriber)21
Publisherͷmap, assign֦ுextension Publisher {func map(_ transform: @escaping (Self.Output) -> T) -> Publishers.Map {return Publishers.Map(upstream: self, transform: transform)}}extension Publisher where Self.Failure == Never {func assign(to keyPath: ReferenceWritableKeyPath,on object: Root) -> AnyCancellable {return Subscribers.Assign(object: object, keyPath: keyPath)}}22
ϝιουνΣΠϯͰΑΓએݴతʹlet cancellable =NotificationCenter.default.publisher(for: .graduated, object: merlin).map { note inreturn note.userInfo?["NewGrade"] as? Int ?? 0}.assign(to: \.grade, on: merlin)23
ϝιουνΣΠϯͰΑΓએݴతʹcompactMapfilterɺprefixͳͲArrayͰݟ׳Εͨؔlet cancellable =NotificationCenter.default.publisher(for: .graduated, object: merlin).compactMap { note inreturn note.userInfo?["NewGrade"] as? Int}.filter { $0 >= 5 }.prefix(3).assign(to: \.grade, on: merlin)24
௨৴ॲཧʹ͑ͦ͏ͳͷଟ͍https://developer.apple.com/documentation/combine/publishers• Zip, Zip3, Zip4, CombineLatest: ͪ߹Θͤ• Decode: σίʔυॲཧ• Retry: ϦτϥΠ• Throttle, Debounce: ࿈ଓ͢ΔΠϕϯτΛݮΒ͢ʢ࿈ଧɺΠϯΫϦϝϯλϧαʔνʣ• Timeout: λΠϜΞτ25
௨৴ॲཧͷ࣮ྫ26
class UserRepository {private var subscriptions = Set()func fetchUsers() -> Future<[User], UserAPIError> {return Future<[User], UserAPIError> { [unowned self] promise inURLSession.shared.dataTaskPublisher(for: url).debounce(for: .milliseconds(500), scheduler: RunLoop.main).retry(3).map { $0.data }.decode(type: [User].self, decoder: JSONDecoder()).receive(on: DispatchQueue.main).sink(receiveCompletion: { _ inpromise(.failure(.somethingWrong))}, receiveValue: { users inpromise(.success(users))}).store(in: &self.subscriptions)}}func cancelAll() {subscriptions.forEach { $0.cancel() }}}27
Viewͱͷ࿈ܞ• SwiftUI• ObservableObject/Published• UIKit• UICollectionViewDiffableDataSource• UITableViewDiffableDataSource28
noppefoxwolf/Combinative• https://github.com/noppefoxwolf/Combinative• UIKitͷUIControllʹCombineΛ֦ுlet button = UIButton()button.cmb.tap.sink { (button) in// do something}@IBOutlet weak var textField: UITextField!textField.cmb.text.sink { (text) inprint(text)}29
͓ΘΓ30