diff --git a/.travis.yml b/.travis.yml index 69f7165c..57230fc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,18 +2,15 @@ osx_image: xcode8 language: objective-c before_install: -- brew update -- if brew outdated | grep -qx xctool; then brew upgrade xctool; fi -- if brew outdated | grep -qx carthage; then brew upgrade carthage; fi -- travis_wait 35 carthage bootstrap --platform iOS,Mac +- travis_wait 35 carthage bootstrap --platform iOS,Mac,tvOS script: -- xcodebuild clean build -project Spots.xcodeproj -scheme "Spots-iOS" -sdk iphonesimulator -- xcodebuild test -project Spots.xcodeproj -scheme "Spots-iOS" -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 6,OS=10.0' -- xcodebuild clean build -project Spots.xcodeproj -scheme "Spots-Mac" -sdk macosx -- xcodebuild test -project Spots.xcodeproj -scheme "Spots-Mac" -sdk macosx -#- xcodebuild clean build -project Spots.xcodeproj -scheme "Spots-tvOS" -destination 'platform=tvOS Simulator,name=Apple TV 1080p,OS=9.2' -#- xcodebuild test -project Spots.xcodeproj -scheme "Spots-tvOS" -destination 'platform=tvOS Simulator,name=Apple TV 1080p,OS=9.2' +- xcodebuild clean build -project Spots.xcodeproj -scheme "Spots-iOS" -sdk iphonesimulator | xcpretty +- xcodebuild test -project Spots.xcodeproj -scheme "Spots-iOS" -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 6,OS=10.0' | xcpretty +- xcodebuild clean build -project Spots.xcodeproj -scheme "Spots-Mac" -sdk macosx | xcpretty +- xcodebuild test -project Spots.xcodeproj -scheme "Spots-Mac" -sdk macosx | xcpretty +- xcodebuild clean build -project Spots.xcodeproj -scheme "Spots-tvOS" -destination 'platform=tvOS Simulator,name=Apple TV 1080p,OS=10.0' | xcpretty +- xcodebuild test -project Spots.xcodeproj -scheme "Spots-tvOS" -destination 'platform=tvOS Simulator,name=Apple TV 1080p,OS=10.0' | xcpretty notifications: email: false diff --git a/Cartfile.resolved b/Cartfile.resolved index d0290b11..036cf61f 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,4 @@ github "krzyzanowskim/CryptoSwift" "0.6.1" github "zenangst/Tailor" "2.0.1" github "hyperoslo/Brick" "2.0.1" -github "hyperoslo/Cache" "2.0.0" +github "hyperoslo/Cache" "2.1.1" diff --git a/Sources/Shared/Extensions/Item+Extensions.swift b/Sources/Shared/Extensions/Item+Extensions.swift index 7383eae5..ebdbce0d 100644 --- a/Sources/Shared/Extensions/Item+Extensions.swift +++ b/Sources/Shared/Extensions/Item+Extensions.swift @@ -100,7 +100,7 @@ public extension Item { if kind != oldItem.kind { return .kind } if newChildren != oldChildren { return .children } if identifier != oldItem.identifier { return .identifier } - if size.height != oldItem.size.height { return .size } + if size != oldItem.size { return .size } if title != oldItem.title { return .title } if subtitle != oldItem.subtitle { return .subtitle } if text != oldItem.text { return .text } diff --git a/Sources/Shared/Extensions/Spotable+Extensions.swift b/Sources/Shared/Extensions/Spotable+Extensions.swift index c565f11a..131f7567 100644 --- a/Sources/Shared/Extensions/Spotable+Extensions.swift +++ b/Sources/Shared/Extensions/Spotable+Extensions.swift @@ -1,7 +1,7 @@ -#if os(iOS) - import UIKit -#else +#if os(OSX) import Foundation +#else + import UIKit #endif import Brick @@ -156,13 +156,21 @@ public extension Spotable { /// Prepare items in component func prepareItems() { - component.items.enumerated().forEach { (index: Int, _) in - configureItem(at: index, usesViewSize: true) + component.items = prepare(items: component.items) + } + + func prepare(items: [Item]) -> [Item] { + var preparedItems = items + preparedItems.enumerated().forEach { (index: Int, item: Item) in + if let configuredItem = configure(item: item, usesViewSize: true) { + preparedItems[index].index = index + preparedItems[index] = configuredItem + } if component.span > 0.0 { #if os(OSX) if let gridable = self as? Gridable, let layout = gridable.layout as? FlowLayout { - component.items[index].size.width = gridable.collectionView.frame.width / CGFloat(component.span) - layout.sectionInset.left - layout.sectionInset.right + preparedItems[index].size.width = gridable.collectionView.frame.width / CGFloat(component.span) - layout.sectionInset.left - layout.sectionInset.right } #else var spotWidth = render().frame.size.width @@ -172,10 +180,12 @@ public extension Spotable { } let newWidth = spotWidth / CGFloat(component.span) - component.items[index].size.width = newWidth + preparedItems[index].size.width = newWidth #endif } } + + return preparedItems } /// Resolve item at index. @@ -290,8 +300,14 @@ public extension Spotable { /// - parameter index: The index of the view model /// - parameter usesViewSize: A boolean value to determine if the view uses the views height public func configureItem(at index: Int, usesViewSize: Bool = false) { - guard var item = item(at: index) else { return } + guard let item = item(at: index), + let configuredItem = configure(item: item, usesViewSize: usesViewSize) else { return } + component.items[index] = configuredItem + } + + func configure(item: Item, usesViewSize: Bool = false) -> Item? { + var item = item item.index = index let kind = item.kind.isEmpty || Self.views.storage[item.kind] == nil @@ -299,7 +315,7 @@ public extension Spotable { : item.kind guard let (_, resolvedView) = Self.views.make(kind), - let view = resolvedView else { return } + let view = resolvedView else { return nil } #if !os(OSX) if let composite = view as? Composable { @@ -348,8 +364,9 @@ public extension Spotable { item.size.width = UIScreen.main.bounds.width / CGFloat(component.span) } #endif - component.items[index] = item } + + return item } /// Update and return the size for the item at index path. diff --git a/Sources/Shared/Extensions/SpotsProtocol+Mutation.swift b/Sources/Shared/Extensions/SpotsProtocol+Mutation.swift index 9d87dd8a..36a58262 100644 --- a/Sources/Shared/Extensions/SpotsProtocol+Mutation.swift +++ b/Sources/Shared/Extensions/SpotsProtocol+Mutation.swift @@ -1,7 +1,7 @@ -#if os(iOS) - import UIKit -#else +#if os(OSX) import Foundation +#else + import UIKit #endif import Brick @@ -44,39 +44,52 @@ extension SpotsProtocol { return } - Dispatch.inQueue(queue: .interactive) { + Dispatch.inQueue(queue: .interactive) { [weak self] in + guard let weakSelf = self else { closure?(); return } + let oldComponents = weakSelf.spots.map { $0.component } let newComponents = components - let oldComponents = self.spots.map { $0.component } guard newComponents !== oldComponents else { Dispatch.mainQueue { closure?() } return } - let oldComponentCount = oldComponents.count - - var changes = [ComponentDiff]() - for (index, component) in components.enumerated() { - if index >= oldComponentCount { - changes.append(.new) - continue - } + let changes = weakSelf.generateChanges(from: newComponents, and: oldComponents) - changes.append(component.diff(component: oldComponents[index])) + weakSelf.process(changes: changes, components: newComponents, withAnimation: animation) { + closure?() } + } + } - if oldComponentCount > components.count { - oldComponents[components.count.. [ComponentDiff] { + let oldComponentCount = oldComponents.count + var changes = [ComponentDiff]() + for (index, component) in components.enumerated() { + if index >= oldComponentCount { + changes.append(.new) + continue } - self.process(changes: changes, components: newComponents, withAnimation: animation) { - closure?() + changes.append(component.diff(component: oldComponents[index])) + } + + if oldComponentCount > components.count { + oldComponents[components.count.. Bool { guard let spot = self.spot(at: index, ofType: Spotable.self) else { return false } - let newItems = newComponents[index].items + let newItems = spot.prepare(items: newComponents[index].items) let oldItems = spot.items guard let diff = Item.evaluate(newItems, oldModels: oldItems) else { closure?(); return false } - let changes = Item.processChanges(diff) + let changes: (ItemChanges) = Item.processChanges(diff) if newItems.count == spot.items.count { - var offsets = [CGPoint]() - spot.reloadIfNeeded(changes, withAnimation: animation, updateDataSource: { - CATransaction.begin() - for item in newItems { - if let compositeSpots = self.compositeSpots[spot.index], - let spots = compositeSpots[item.index] { - for spot in spots { - offsets.append(spot.render().contentOffset) - } + reload(in: spot, with: changes, newItems: newItems, animation: animation, closure: closure) + } else if newItems.count < spot.items.count { + reloadLess(in: spot, with: changes, newItems: newItems, animation: animation, closure: closure) + } else if newItems.count > spot.items.count { + reloadMore(in: spot, with: changes, newItems: newItems, animation: animation, closure: closure) + } + + return false + } + + /// Reload Spotable object with changes and new items. + /// + /// - parameter spot: The spotable object that should be updated. + /// - parameter changes: A ItemChanges tuple. + /// - parameter newItems: The new items that should be used to updated the data source. + /// - parameter animation: The animation that should be used when updating. + /// - parameter closure: A completion closure. + private func reload(in spot: Spotable, + with changes: (ItemChanges), + newItems: [Item], + animation: Animation, + closure: (() -> Void)? = nil) { + var offsets = [CGPoint]() + spot.reloadIfNeeded(changes, withAnimation: animation, updateDataSource: { + if spot is Gridable { CATransaction.begin() } + for item in newItems { + if let compositeSpots = self.compositeSpots[spot.index], + let spots = compositeSpots[item.index] { + for spot in spots { + offsets.append(spot.render().contentOffset) } } + } - spot.items = newItems - }) { [weak self] in - for item in newItems { - if let compositeSpots = self?.compositeSpots[spot.index], - let spots = compositeSpots[item.index] { - for (index, spot) in spots.enumerated() { - guard index < offsets.count else { continue } - spot.render().contentOffset = offsets[index] - } + spot.items = newItems + }) { [weak self] in + for item in newItems { + if let compositeSpots = self?.compositeSpots[spot.index], + let spots = compositeSpots[item.index] { + for (index, spot) in spots.enumerated() { + guard index < offsets.count else { continue } + spot.render().contentOffset = offsets[index] } } + } + + closure?() + self?.scrollView.layoutSubviews() + if spot is Gridable { CATransaction.commit() } + } + } + /// Reload Spotable object with less items + /// + /// - parameter spot: The spotable object that should be updated. + /// - parameter changes: A ItemChanges tuple. + /// - parameter newItems: The new items that should be used to updated the data source. + /// - parameter animation: The animation that should be used when updating. + /// - parameter closure: A completion closure. + private func reloadLess(in spot: Spotable, + with changes: (ItemChanges), + newItems: [Item], + animation: Animation, + closure: (() -> Void)? = nil) { + spot.reloadIfNeeded(changes, withAnimation: animation, updateDataSource: { + if spot is Gridable { CATransaction.begin() } + spot.items = newItems + }) { [weak self] in + guard !newItems.isEmpty else { closure?() self?.scrollView.layoutSubviews() - CATransaction.commit() + if spot is Gridable { CATransaction.commit() } + return } - } else if newItems.count < spot.items.count { - spot.reloadIfNeeded(changes, withAnimation: animation, updateDataSource: { - CATransaction.begin() - spot.items = newItems - }) { [weak self] in - guard !newItems.isEmpty else { - closure?() - self?.scrollView.layoutSubviews() - CATransaction.commit() - return - } - let executeClosure = newItems.count - 1 - for (index, item) in newItems.enumerated() { - let components = Parser.parse(item.children).map { $0.component } - if let compositeSpots = self?.compositeSpots[spot.index], - let spots = compositeSpots[item.index] { - for (index, removedSpot) in spots.enumerated() { - guard !components.contains(removedSpot.component) else { continue } - let oldContent = self?.compositeSpots[spot.index]?[item.index] - if var oldContent = self?.compositeSpots[spot.index]?[item.index], index < oldContent.count { - oldContent.remove(at: index) - } - self?.compositeSpots[spot.index]?[item.index] = oldContent + let executeClosure = newItems.count - 1 + for (index, item) in newItems.enumerated() { + let components = Parser.parse(item.children).map { $0.component } + if let compositeSpots = self?.compositeSpots[spot.index], + let spots = compositeSpots[item.index] { + for (index, removedSpot) in spots.enumerated() { + guard !components.contains(removedSpot.component) else { continue } + let oldContent = self?.compositeSpots[spot.index]?[item.index] + if var oldContent = self?.compositeSpots[spot.index]?[item.index], index < oldContent.count { + oldContent.remove(at: index) } - } - spot.update(item, index: index, withAnimation: animation) { - guard index == executeClosure else { return } - closure?() - self?.scrollView.layoutSubviews() - CATransaction.commit() + self?.compositeSpots[spot.index]?[item.index] = oldContent } } - } - } else if newItems.count > spot.items.count { - spot.reloadIfNeeded(changes, withAnimation: animation, updateDataSource: { - CATransaction.begin() - spot.items = newItems - }) { - spot.reload(nil, withAnimation: animation) { [weak self] in + spot.update(item, index: index, withAnimation: animation) { + guard index == executeClosure else { return } closure?() self?.scrollView.layoutSubviews() - Dispatch.delay(for: 0.1) { - CATransaction.commit() - } + if spot is Gridable { CATransaction.commit() } } } } + } - return false + /// Reload Spotable object with more items + /// + /// - parameter spot: The spotable object that should be updated. + /// - parameter changes: A ItemChanges tuple. + /// - parameter newItems: The new items that should be used to updated the data source. + /// - parameter animation: The animation that should be used when updating. + /// - parameter closure: A completion closure. + private func reloadMore(in spot: Spotable, + with changes: (ItemChanges), + newItems: [Item], + animation: Animation, + closure: (() -> Void)? = nil) { + spot.reloadIfNeeded(changes, withAnimation: animation, updateDataSource: { + if spot is Gridable { CATransaction.begin() } + spot.items = newItems + }) { + if !spot.items.filter({ !$0.children.isEmpty }).isEmpty { + spot.reload(nil, withAnimation: animation) { + if spot is Gridable { CATransaction.commit() } + closure?() + } + } else { + spot.updateHeight() { [weak self] in + + self?.scrollView.layoutSubviews() + if spot is Gridable { CATransaction.commit() } + closure?() + } + } + } } func process(changes: [ComponentDiff], - components newComponents: [Component], - withAnimation animation: Animation = .automatic, - closure: Completion = nil) { + components newComponents: [Component], + withAnimation animation: Animation = .automatic, + closure: Completion = nil) { Dispatch.mainQueue { [weak self] in guard let weakSelf = self else { closure?(); return } var yOffset: CGFloat = 0.0 var runClosure = true + for (index, change) in changes.enumerated() { switch change { case .identifier, .kind, .span, .header, .meta: @@ -236,13 +298,19 @@ extension SpotsProtocol { weakSelf.removeSpot(at: index) case .items: runClosure = weakSelf.setupItemsForSpot(index, - newComponents: newComponents, - withAnimation: animation, - closure: closure) + newComponents: newComponents, + withAnimation: animation, + closure: closure) case .none: break } } + for removedSpot in weakSelf.spots where removedSpot.render().superview == nil { + if let index = weakSelf.spots.index(where: { removedSpot.component == $0.component }) { + weakSelf.spots.remove(at: index) + } + } + if runClosure { closure?() weakSelf.scrollView.layoutSubviews() @@ -385,7 +453,7 @@ extension SpotsProtocol { update(spotAtIndex: index, withAnimation: animation, withCompletion: completion, { [weak self] in $0.items = items self?.scrollView.layoutSubviews() - }) + }) } /** @@ -445,13 +513,13 @@ extension SpotsProtocol { spot(at: spotIndex, ofType: Spotable.self)?.refreshIndexes() } - /** - - parameter item: The view model that you want to update - - parameter index: The index that you want to insert the view model at - - parameter spotIndex: The index of the spot that you want to update into - - parameter animation: A Animation struct that determines which animation that should be used to perform the update - - parameter completion: A completion closure that will run after the spot has performed updates internally - */ + /// Update item at index inside a specific Spotable object + /// + /// - parameter item: The view model that you want to update. + /// - parameter index: The index that you want to insert the view model at. + /// - parameter spotIndex: The index of the spot that you want to update into. + /// - parameter animation: A Animation struct that determines which animation that should be used to perform the update. + /// - parameter completion: A completion closure that will run after the spot has performed updates internally. public func update(_ item: Item, index: Int = 0, spotIndex: Int, withAnimation animation: Animation = .none, completion: Completion = nil) { guard let oldItem = spot(at: spotIndex, ofType: Spotable.self)?.item(at: index), item != oldItem else { diff --git a/Sources/Shared/Protocols/Componentable.swift b/Sources/Shared/Protocols/Componentable.swift index edd59dcb..23c932f7 100644 --- a/Sources/Shared/Protocols/Componentable.swift +++ b/Sources/Shared/Protocols/Componentable.swift @@ -1,7 +1,7 @@ -#if os(iOS) - import UIKit -#else +#if os(OSX) import Foundation +#else + import UIKit #endif /// A protocol for Componentable objects. diff --git a/Sources/Shared/Protocols/SpotConfigurable.swift b/Sources/Shared/Protocols/SpotConfigurable.swift index 4038a01c..f4e545e6 100644 --- a/Sources/Shared/Protocols/SpotConfigurable.swift +++ b/Sources/Shared/Protocols/SpotConfigurable.swift @@ -1,7 +1,7 @@ -#if os(iOS) - import UIKit -#else +#if os(OSX) import Foundation +#else + import UIKit #endif import Brick diff --git a/Sources/Shared/Protocols/SpotsProtocol.swift b/Sources/Shared/Protocols/SpotsProtocol.swift index 4ee69b9b..7ff28b37 100644 --- a/Sources/Shared/Protocols/SpotsProtocol.swift +++ b/Sources/Shared/Protocols/SpotsProtocol.swift @@ -1,7 +1,7 @@ -#if os(iOS) - import UIKit -#else +#if os(OSX) import Foundation +#else + import UIKit #endif import Brick diff --git a/Sources/Shared/Structs/Component.swift b/Sources/Shared/Structs/Component.swift index ba002cb3..3a04beab 100644 --- a/Sources/Shared/Structs/Component.swift +++ b/Sources/Shared/Structs/Component.swift @@ -1,7 +1,7 @@ -#if os(iOS) - import UIKit -#else +#if os(OSX) import Foundation +#else + import UIKit #endif import Tailor diff --git a/Sources/iOS/Extensions/Listable+Extensions+iOS.swift b/Sources/iOS/Extensions/Listable+Extensions+iOS.swift index e17138e9..cb205eab 100644 --- a/Sources/iOS/Extensions/Listable+Extensions+iOS.swift +++ b/Sources/iOS/Extensions/Listable+Extensions+iOS.swift @@ -312,8 +312,8 @@ extension Listable { : tableView.reloadData() } - UIView.setAnimationsEnabled(true) - updateHeight() - completion?() + updateHeight() { + completion?() + } } } diff --git a/Spots.xcodeproj/project.pbxproj b/Spots.xcodeproj/project.pbxproj index f018f8bd..7e92d452 100644 --- a/Spots.xcodeproj/project.pbxproj +++ b/Spots.xcodeproj/project.pbxproj @@ -164,12 +164,10 @@ BD4295521D81D32400E07E1C /* TestListSpot.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4295511D81D32400E07E1C /* TestListSpot.swift */; }; BD4295531D81D32400E07E1C /* TestListSpot.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4295511D81D32400E07E1C /* TestListSpot.swift */; }; BD4295551D81D39700E07E1C /* TestCarouselSpot.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4295541D81D39700E07E1C /* TestCarouselSpot.swift */; }; - BD4295561D81D39700E07E1C /* TestCarouselSpot.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4295541D81D39700E07E1C /* TestCarouselSpot.swift */; }; BD4295581D81D45D00E07E1C /* TestViewSpot.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4295571D81D45D00E07E1C /* TestViewSpot.swift */; }; BD4295591D81D45D00E07E1C /* TestViewSpot.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD4295571D81D45D00E07E1C /* TestViewSpot.swift */; }; BD7397381D718CDB000AF2DE /* TestComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D584782B1C43FF34006EBA49 /* TestComponent.swift */; }; BD73973A1D718CDB000AF2DE /* TestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58478281C43FF34006EBA49 /* TestFactory.swift */; }; - BD73973B1D718CDB000AF2DE /* TestController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58478291C43FF34006EBA49 /* TestController.swift */; }; BD7397401D718CDB000AF2DE /* Spots.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D58478091C43FEB8006EBA49 /* Spots.framework */; }; BD73974B1D718D38000AF2DE /* CollectionAdapter+tvOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD73974A1D718D38000AF2DE /* CollectionAdapter+tvOS.swift */; }; BD8B1EB21D93D813000C3F53 /* GrandCentralDispatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8B1EB11D93D813000C3F53 /* GrandCentralDispatch.swift */; }; @@ -181,7 +179,6 @@ BDD9ABCE1DB53475005D8C04 /* TestCarouselComposite.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDD9ABCD1DB53475005D8C04 /* TestCarouselComposite.swift */; }; BDD9ABCF1DB53475005D8C04 /* TestCarouselComposite.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDD9ABCD1DB53475005D8C04 /* TestCarouselComposite.swift */; }; BDEED2E81D8446400030B475 /* TestSpotsScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDEED2E71D8446400030B475 /* TestSpotsScrollView.swift */; }; - BDEED2E91D8446400030B475 /* TestSpotsScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDEED2E71D8446400030B475 /* TestSpotsScrollView.swift */; }; D2FEDC1F1D9E845A004D5ABF /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2FEDC1E1D9E845A004D5ABF /* UIViewController+Extensions.swift */; }; D58478141C43FEB9006EBA49 /* Spots.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D58478091C43FEB8006EBA49 /* Spots.framework */; }; D58478531C43FFEB006EBA49 /* TestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = D58478281C43FF34006EBA49 /* TestFactory.swift */; }; @@ -194,12 +191,12 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - BD7397331D718CDB000AF2DE /* PBXContainerItemProxy */ = { + BD53211E1DBD38EC007D6C25 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = D58478001C43FEB8006EBA49 /* Project object */; proxyType = 1; - remoteGlobalIDString = D58478081C43FEB8006EBA49; - remoteInfo = Spots; + remoteGlobalIDString = BD7396FD1D718CD2000AF2DE; + remoteInfo = "Spots-tvOS"; }; D58478151C43FEB9006EBA49 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -748,7 +745,7 @@ buildRules = ( ); dependencies = ( - BD7397321D718CDB000AF2DE /* PBXTargetDependency */, + BD53211F1DBD38EC007D6C25 /* PBXTargetDependency */, ); name = "Spots-tvOS-Tests"; productName = SpotsTests; @@ -1082,13 +1079,10 @@ buildActionMask = 2147483647; files = ( BD4295591D81D45D00E07E1C /* TestViewSpot.swift in Sources */, - BD4295561D81D39700E07E1C /* TestCarouselSpot.swift in Sources */, BD7397381D718CDB000AF2DE /* TestComponent.swift in Sources */, BD4295501D81CE4F00E07E1C /* TestGridSpot.swift in Sources */, - BDEED2E91D8446400030B475 /* TestSpotsScrollView.swift in Sources */, BD4295531D81D32400E07E1C /* TestListSpot.swift in Sources */, BD73973A1D718CDB000AF2DE /* TestFactory.swift in Sources */, - BD73973B1D718CDB000AF2DE /* TestController.swift in Sources */, BD01BD131DAEA523009C10FF /* TestParser.swift in Sources */, BD01BD1A1DAEA7FD009C10FF /* TestListComposite.swift in Sources */, BD10D5271D7955AC00DF8E9B /* TestViewModelExtensions.swift in Sources */, @@ -1240,10 +1234,10 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - BD7397321D718CDB000AF2DE /* PBXTargetDependency */ = { + BD53211F1DBD38EC007D6C25 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = D58478081C43FEB8006EBA49 /* Spots-iOS */; - targetProxy = BD7397331D718CDB000AF2DE /* PBXContainerItemProxy */; + target = BD7396FD1D718CD2000AF2DE /* Spots-tvOS */; + targetProxy = BD53211E1DBD38EC007D6C25 /* PBXContainerItemProxy */; }; D58478161C43FEB9006EBA49 /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -1319,7 +1313,7 @@ buildSettings = { FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/Carthage/Build/tvOS", ); INFOPLIST_FILE = "SpotsTests/Info-tvOS.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; @@ -1335,9 +1329,9 @@ buildSettings = { FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Carthage/Build/iOS", + "$(PROJECT_DIR)/Carthage/Build/tvOS", ); - INFOPLIST_FILE = "Spots-iOS-Tests copy-Info.plist"; + INFOPLIST_FILE = "SpotsTests/Info-tvOS.plist"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks $(FRAMEWORK_SEARCH_PATHS)"; PRODUCT_BUNDLE_IDENTIFIER = no.hyper.SpotsTests; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Spots.xcodeproj/xcshareddata/xcschemes/Spots-tvOS.xcscheme b/Spots.xcodeproj/xcshareddata/xcschemes/Spots-tvOS.xcscheme index 295a1264..2ae776c0 100644 --- a/Spots.xcodeproj/xcshareddata/xcschemes/Spots-tvOS.xcscheme +++ b/Spots.xcodeproj/xcshareddata/xcschemes/Spots-tvOS.xcscheme @@ -20,6 +20,20 @@ ReferencedContainer = "container:Spots.xcodeproj"> + + + + 0) - XCTAssert(spotController.spot!.component.items == items) + XCTAssert(self.controller.spot!.component.items.count > 0) + XCTAssert(self.controller.spot!.component.items == items) // Test appending items without kind let exception = self.expectation(description: "Test append items") - spotController.append([ + controller.append([ Item(title: "title3"), Item(title: "title4") ], spotIndex: 0) { - XCTAssertEqual(spotController.spot!.component.items.count, 4) - XCTAssertEqual(spotController.spot!.component.items[2].title, "title3") - XCTAssertEqual(spotController.spot!.component.items[3].title, "title4") + XCTAssertEqual(self.controller.spot!.component.items.count, 4) + XCTAssertEqual(self.controller.spot!.component.items[2].title, "title3") + XCTAssertEqual(self.controller.spot!.component.items[3].title, "title4") exception.fulfill() } waitForExpectations(timeout: 0.1, handler: nil) @@ -83,24 +89,24 @@ class ControllerTests : XCTestCase { func testPrependItemsInListSpot() { let component = Component(title: "Component", kind: "list") let listSpot = ListSpot(component: component) - let spotController = Controller(spot: listSpot) + controller = Controller(spot: listSpot) let items = [ Item(title: "title1", kind: "list"), Item(title: "title2", kind: "list") ] - spotController.prepend(items, spotIndex: 0) + controller.prepend(items, spotIndex: 0) - XCTAssertEqual(spotController.spot!.component.items.count, 2) - XCTAssert(spotController.spot!.component.items == items) + XCTAssertEqual(self.controller.spot!.component.items.count, 2) + XCTAssert(self.controller.spot!.component.items == items) let exception = self.expectation(description: "Test prepend items") - spotController.prepend([ + controller.prepend([ Item(title: "title3"), Item(title: "title4") ], spotIndex: 0) { - XCTAssertEqual(spotController.spot!.component.items[0].title, "title3") - XCTAssertEqual(spotController.spot!.component.items[1].title, "title4") + XCTAssertEqual(self.controller.spot!.component.items[0].title, "title3") + XCTAssertEqual(self.controller.spot!.component.items[1].title, "title4") exception.fulfill() } waitForExpectations(timeout: 0.1, handler: nil) @@ -113,22 +119,22 @@ class ControllerTests : XCTestCase { ]) let initialListSpot = ListSpot(component: component) - let spotController = Controller(spot: initialListSpot) + controller = Controller(spot: initialListSpot) - let firstItem = spotController.spot!.component.items.first + let firstItem = self.controller.spot!.component.items.first XCTAssertEqual(firstItem?.title, "title1") XCTAssertEqual(firstItem?.index, 0) let exception = self.expectation(description: "Test delete item") - let listSpot = (spotController.spot as! ListSpot) + let listSpot = (self.controller.spot as! ListSpot) listSpot.delete(component.items.first!) { - let lastItem = spotController.spot!.component.items.first + let lastItem = self.controller.spot!.component.items.first XCTAssertNotEqual(lastItem?.title, "title1") - XCTAssertEqual(lastItem?.index, 1) + XCTAssertEqual(lastItem?.index, 0) XCTAssertEqual(lastItem?.title, "title2") - XCTAssertEqual(spotController.spot!.component.items.count, 1) + XCTAssertEqual(self.controller.spot!.component.items.count, 1) exception.fulfill() } waitForExpectations(timeout: 0.1, handler: nil) @@ -137,24 +143,24 @@ class ControllerTests : XCTestCase { func testAppendItemInGridSpot() { let component = Component(title: "Component", kind: "grid") let listSpot = ListSpot(component: component) - let spotController = Controller(spot: listSpot) + controller = Controller(spot: listSpot) - XCTAssert(spotController.spot!.component.items.count == 0) + XCTAssert(self.controller.spot!.component.items.count == 0) let item = Item(title: "title1", kind: "grid") - spotController.append(item, spotIndex: 0) + controller.append(item, spotIndex: 0) - XCTAssert(spotController.spot!.component.items.count == 1) + XCTAssert(self.controller.spot!.component.items.count == 1) - if let testItem = spotController.spot!.component.items.first { + if let testItem = self.controller.spot!.component.items.first { XCTAssert(testItem == item) } // Test appending item without kind let exception = self.expectation(description: "Test append item") - spotController.append(Item(title: "title2"), spotIndex: 0) { - XCTAssert(spotController.spot!.component.items.count == 2) - XCTAssertEqual(spotController.spot!.component.items[1].title, "title2") + controller.append(Item(title: "title2"), spotIndex: 0) { + XCTAssert(self.controller.spot!.component.items.count == 2) + XCTAssertEqual(self.controller.spot!.component.items[1].title, "title2") exception.fulfill() } waitForExpectations(timeout: 0.1, handler: nil) @@ -163,26 +169,26 @@ class ControllerTests : XCTestCase { func testAppendItemsInGridSpot() { let component = Component(title: "Component", kind: "grid") let listSpot = ListSpot(component: component) - let spotController = Controller(spot: listSpot) + controller = Controller(spot: listSpot) let items = [ Item(title: "title1", kind: "grid"), Item(title: "title2", kind: "grid") ] - spotController.append(items, spotIndex: 0) + controller.append(items, spotIndex: 0) - XCTAssert(spotController.spot!.component.items.count > 0) - XCTAssert(spotController.spot!.component.items == items) + XCTAssert(self.controller.spot!.component.items.count > 0) + XCTAssert(self.controller.spot!.component.items == items) // Test appending items without kind let exception = self.expectation(description: "Test append items") - spotController.append([ + controller.append([ Item(title: "title3"), Item(title: "title4") ], spotIndex: 0) { - XCTAssertEqual(spotController.spot!.component.items.count, 4) - XCTAssertEqual(spotController.spot!.component.items[2].title, "title3") - XCTAssertEqual(spotController.spot!.component.items[3].title, "title4") + XCTAssertEqual(self.controller.spot!.component.items.count, 4) + XCTAssertEqual(self.controller.spot!.component.items[2].title, "title3") + XCTAssertEqual(self.controller.spot!.component.items[3].title, "title4") exception.fulfill() } waitForExpectations(timeout: 0.1, handler: nil) @@ -191,24 +197,24 @@ class ControllerTests : XCTestCase { func testPrependItemsInGridSpot() { let component = Component(title: "Component", kind: "grid") let listSpot = ListSpot(component: component) - let spotController = Controller(spot: listSpot) + controller = Controller(spot: listSpot) let items = [ Item(title: "title1", kind: "grid"), Item(title: "title2", kind: "grid") ] - spotController.prepend(items, spotIndex: 0) + controller.prepend(items, spotIndex: 0) - XCTAssertEqual(spotController.spot!.component.items.count, 2) - XCTAssert(spotController.spot!.component.items == items) + XCTAssertEqual(self.controller.spot!.component.items.count, 2) + XCTAssert(self.controller.spot!.component.items == items) let exception = self.expectation(description: "Test prepend items") - spotController.prepend([ + controller.prepend([ Item(title: "title3"), Item(title: "title4") ], spotIndex: 0) { - XCTAssertEqual(spotController.spot!.component.items[0].title, "title3") - XCTAssertEqual(spotController.spot!.component.items[1].title, "title4") + XCTAssertEqual(self.controller.spot!.component.items[0].title, "title3") + XCTAssertEqual(self.controller.spot!.component.items[1].title, "title4") exception.fulfill() } waitForExpectations(timeout: 0.1, handler: nil) @@ -221,22 +227,22 @@ class ControllerTests : XCTestCase { ]) let initialListSpot = ListSpot(component: component) - let spotController = Controller(spot: initialListSpot) + controller = Controller(spot: initialListSpot) - let firstItem = spotController.spot!.component.items.first + let firstItem = self.controller.spot!.component.items.first XCTAssertEqual(firstItem?.title, "title1") XCTAssertEqual(firstItem?.index, 0) let exception = self.expectation(description: "Test delete item") - let listSpot = (spotController.spot as! ListSpot) + let listSpot = (self.controller.spot as! ListSpot) listSpot.delete(component.items.first!) { - let lastItem = spotController.spot!.component.items.first + let lastItem = self.controller.spot!.component.items.first XCTAssertNotEqual(lastItem?.title, "title1") - XCTAssertEqual(lastItem?.index, 1) + XCTAssertEqual(lastItem?.index, 0) XCTAssertEqual(lastItem?.title, "title2") - XCTAssertEqual(spotController.spot!.component.items.count, 1) + XCTAssertEqual(self.controller.spot!.component.items.count, 1) exception.fulfill() } waitForExpectations(timeout: 0.1, handler: nil) @@ -245,24 +251,24 @@ class ControllerTests : XCTestCase { func testAppendItemInCarouselSpot() { let component = Component(title: "Component", kind: "carousel") let listSpot = ListSpot(component: component) - let spotController = Controller(spot: listSpot) + controller = Controller(spot: listSpot) - XCTAssert(spotController.spot!.component.items.count == 0) + XCTAssert(self.controller.spot!.component.items.count == 0) let item = Item(title: "title1", kind: "carousel") - spotController.append(item, spotIndex: 0) + controller.append(item, spotIndex: 0) - XCTAssert(spotController.spot!.component.items.count == 1) + XCTAssert(self.controller.spot!.component.items.count == 1) - if let testItem = spotController.spot!.component.items.first { + if let testItem = self.controller.spot!.component.items.first { XCTAssert(testItem == item) } // Test appending item without kind let exception = self.expectation(description: "Test append item") - spotController.append(Item(title: "title2"), spotIndex: 0) { - XCTAssert(spotController.spot!.component.items.count == 2) - XCTAssertEqual(spotController.spot!.component.items[1].title, "title2") + controller.append(Item(title: "title2"), spotIndex: 0) { + XCTAssert(self.controller.spot!.component.items.count == 2) + XCTAssertEqual(self.controller.spot!.component.items[1].title, "title2") exception.fulfill() } waitForExpectations(timeout: 0.1, handler: nil) @@ -271,26 +277,26 @@ class ControllerTests : XCTestCase { func testAppendItemsInCarouselSpot() { let component = Component(title: "Component", kind: "carousel") let listSpot = ListSpot(component: component) - let spotController = Controller(spot: listSpot) + controller = Controller(spot: listSpot) let items = [ Item(title: "title1", kind: "carousel"), Item(title: "title2", kind: "carousel") ] - spotController.append(items, spotIndex: 0) + controller.append(items, spotIndex: 0) - XCTAssert(spotController.spot!.component.items.count > 0) - XCTAssert(spotController.spot!.component.items == items) + XCTAssert(self.controller.spot!.component.items.count > 0) + XCTAssert(self.controller.spot!.component.items == items) // Test appending items without kind let exception = self.expectation(description: "Test append items") - spotController.append([ + controller.append([ Item(title: "title3"), Item(title: "title4") ], spotIndex: 0) { - XCTAssertEqual(spotController.spot!.component.items.count, 4) - XCTAssertEqual(spotController.spot!.component.items[2].title, "title3") - XCTAssertEqual(spotController.spot!.component.items[3].title, "title4") + XCTAssertEqual(self.controller.spot!.component.items.count, 4) + XCTAssertEqual(self.controller.spot!.component.items[2].title, "title3") + XCTAssertEqual(self.controller.spot!.component.items[3].title, "title4") exception.fulfill() } waitForExpectations(timeout: 0.1, handler: nil) @@ -299,24 +305,24 @@ class ControllerTests : XCTestCase { func testPrependItemsInCarouselSpot() { let component = Component(title: "Component", kind: "carousel") let listSpot = ListSpot(component: component) - let spotController = Controller(spot: listSpot) + controller = Controller(spot: listSpot) let items = [ Item(title: "title1", kind: "carousel"), Item(title: "title2", kind: "carousel") ] - spotController.prepend(items, spotIndex: 0) + controller.prepend(items, spotIndex: 0) - XCTAssertEqual(spotController.spot!.component.items.count, 2) - XCTAssert(spotController.spot!.component.items == items) + XCTAssertEqual(self.controller.spot!.component.items.count, 2) + XCTAssert(self.controller.spot!.component.items == items) let exception = self.expectation(description: "Test prepend items") - spotController.prepend([ + controller.prepend([ Item(title: "title3"), Item(title: "title4") ], spotIndex: 0) { - XCTAssertEqual(spotController.spot!.component.items[0].title, "title3") - XCTAssertEqual(spotController.spot!.component.items[1].title, "title4") + XCTAssertEqual(self.controller.spot!.component.items[0].title, "title3") + XCTAssertEqual(self.controller.spot!.component.items[1].title, "title4") exception.fulfill() } waitForExpectations(timeout: 0.1, handler: nil) @@ -329,22 +335,22 @@ class ControllerTests : XCTestCase { ]) let initialListSpot = ListSpot(component: component) - let spotController = Controller(spot: initialListSpot) + controller = Controller(spot: initialListSpot) - let firstItem = spotController.spot!.component.items.first + let firstItem = self.controller.spot!.component.items.first XCTAssertEqual(firstItem?.title, "title1") XCTAssertEqual(firstItem?.index, 0) let exception = self.expectation(description: "Test delete item") - let listSpot = (spotController.spot as! ListSpot) + let listSpot = (self.controller.spot as! ListSpot) listSpot.delete(component.items.first!) { - let lastItem = spotController.spot!.component.items.first + let lastItem = self.controller.spot!.component.items.first XCTAssertNotEqual(lastItem?.title, "title1") - XCTAssertEqual(lastItem?.index, 1) + XCTAssertEqual(lastItem?.index, 0) XCTAssertEqual(lastItem?.title, "title2") - XCTAssertEqual(spotController.spot!.component.items.count, 1) + XCTAssertEqual(self.controller.spot!.component.items.count, 1) exception.fulfill() } waitForExpectations(timeout: 0.1, handler: nil) @@ -369,18 +375,18 @@ class ControllerTests : XCTestCase { let listSpot = ListSpot(component: Component(title: "ListSpot")) let listSpot2 = ListSpot(component: Component(title: "ListSpot2")) let gridSpot = GridSpot(component: Component(title: "GridSpot", items: [Item(title: "Item")])) - let spotController = Controller(spots: [listSpot, listSpot2, gridSpot]) - - XCTAssertNotNil(spotController.resolve(spot: { $1.component.title == "ListSpot" })) - XCTAssertNotNil(spotController.resolve(spot: { $1.component.title == "GridSpot" })) - XCTAssertNotNil(spotController.resolve(spot: { $1 is Listable })) - XCTAssertNotNil(spotController.resolve(spot: { $1 is Gridable })) - XCTAssertNotNil(spotController.resolve(spot: { $1.items.filter{ $0.title == "Item" }.first != nil })) - XCTAssertEqual(spotController.resolve(spot: { $0.0 == 0 })?.component.title, "ListSpot") - XCTAssertEqual(spotController.resolve(spot: { $0.0 == 1 })?.component.title, "ListSpot2") - XCTAssertEqual(spotController.resolve(spot: { $0.0 == 2 })?.component.title, "GridSpot") - - XCTAssert(spotController.filter(spots: { $0 is Listable }).count == 2) + controller = Controller(spots: [listSpot, listSpot2, gridSpot]) + + XCTAssertNotNil(self.controller.resolve(spot: { $1.component.title == "ListSpot" })) + XCTAssertNotNil(self.controller.resolve(spot: { $1.component.title == "GridSpot" })) + XCTAssertNotNil(self.controller.resolve(spot: { $1 is Listable })) + XCTAssertNotNil(self.controller.resolve(spot: { $1 is Gridable })) + XCTAssertNotNil(self.controller.resolve(spot: { $1.items.filter{ $0.title == "Item" }.first != nil })) + XCTAssertEqual(self.controller.resolve(spot: { $0.0 == 0 })?.component.title, "ListSpot") + XCTAssertEqual(self.controller.resolve(spot: { $0.0 == 1 })?.component.title, "ListSpot2") + XCTAssertEqual(self.controller.resolve(spot: { $0.0 == 2 })?.component.title, "GridSpot") + + XCTAssert(self.controller.filter(spots: { $0 is Listable }).count == 2) } func testJSONInitialiser() { @@ -453,7 +459,7 @@ class ControllerTests : XCTestCase { XCTAssertTrue(firstController.spots.first!.component == secondController.spots.first!.component) } - func testReloadIfNeededWithComponents() { + func testReloadIfNeededWithJSON() { let initialJSON: [String : Any] = [ "components" : [ ["kind" : "list", @@ -494,36 +500,245 @@ class ControllerTests : XCTestCase { ] ] - let controller = Controller(initialJSON) - XCTAssertTrue(controller.spots[0] is ListSpot) - XCTAssertEqual(controller.spots[0].items.first?.title, "First list item") - XCTAssertEqual(controller.spots[1].items.first?.title, "First list item") - XCTAssertTrue(controller.spots[1] is ListSpot) - XCTAssertTrue(controller.spots.count == 2) - XCTAssertTrue(controller.compositeSpots.count == 0) + controller = Controller(initialJSON) + XCTAssertTrue(self.controller.spots[0] is ListSpot) + XCTAssertEqual(self.controller.spots[0].items.first?.title, "First list item") + XCTAssertEqual(self.controller.spots[1].items.first?.title, "First list item") + XCTAssertTrue(self.controller.spots[1] is ListSpot) + XCTAssertTrue(self.controller.spots.count == 2) + XCTAssertTrue(self.controller.compositeSpots.count == 0) let exception = self.expectation(description: "Reload multiple times with JSON (if needed)") controller.reloadIfNeeded(newJSON) { - XCTAssertEqual(controller.spots.count, 2) - XCTAssertTrue(controller.spots[0] is ListSpot) - XCTAssertTrue(controller.spots[1] is GridSpot) - XCTAssertEqual(controller.spots[0].items.first?.title, "First list item 2") - XCTAssertEqual(controller.spots[1].items.first?.title, "First list item") - - XCTAssertEqual(controller.spots[0].items[1].kind, "composite") - XCTAssertEqual(controller.compositeSpots.count, 1) - - controller.reloadIfNeeded(initialJSON) { - XCTAssertTrue(controller.spots[0] is ListSpot) - XCTAssertEqual(controller.spots[0].items.first?.title, "First list item") - XCTAssertEqual(controller.spots[1].items.first?.title, "First list item") - XCTAssertTrue(controller.spots[1] is ListSpot) - XCTAssertTrue(controller.spots.count == 2) - XCTAssertTrue(controller.compositeSpots.count == 0) + XCTAssertEqual(self.controller.spots.count, 2) + XCTAssertTrue(self.controller.spots[0] is ListSpot) + XCTAssertTrue(self.controller.spots[1] is GridSpot) + XCTAssertEqual(self.controller.spots[0].items.first?.title, "First list item 2") + XCTAssertEqual(self.controller.spots[1].items.first?.title, "First list item") + + XCTAssertEqual(self.controller.spots[0].items[1].kind, "composite") + XCTAssertEqual(self.controller.compositeSpots.count, 1) + + self.controller.reloadIfNeeded(initialJSON) { + XCTAssertTrue(self.controller.spots[0] is ListSpot) + XCTAssertEqual(self.controller.spots[0].items.first?.title, "First list item") + XCTAssertEqual(self.controller.spots[1].items.first?.title, "First list item") + XCTAssertTrue(self.controller.spots[1] is ListSpot) + XCTAssertTrue(self.controller.spots.count == 2) + XCTAssertTrue(self.controller.compositeSpots.count == 0) exception.fulfill() } } waitForExpectations(timeout: 0.1, handler: nil) } + + func testControllerItemChanges() { + let initialComponents = [ + Component( + kind: "list", + items: [ + Item(title: "Fullname", subtitle: "Job title", kind: "image"), + Item(title: "Follow", kind: "toggle", meta: ["dynamic-height" : true]), + Item(title: "First name", subtitle: "Input first name",kind: "info"), + Item(title: "Last name", subtitle: "Input last name",kind: "info"), + Item(title: "Twitter", subtitle: "@twitter",kind: "info"), + Item(title: "", subtitle: "Biography", kind: "core", meta: ["dynamic-height" : true]) + ] + ) + ] + + let newComponents = [ + Component( + kind: "list", + items: [ + Item(title: "Fullname", subtitle: "Job title", text: "Bot", kind: "image"), + Item(title: "Follow", kind: "toggle", meta: ["dynamic-height" : true]), + Item(title: "First name", subtitle: "Input first name", text: "John", kind: "info"), + Item(title: "Last name", subtitle: "Input last name", text: "Hyperseed", kind: "info"), + Item(title: "Twitter", subtitle: "@johnhyperseed",kind: "info"), + Item(subtitle: "Biography", text: "John Hyperseed is a bot", kind: "core", meta: ["dynamic-height" : true]) + ] + ) + ] + + let spots = initialComponents.map { Factory.resolve(component: $0) } + controller = Controller(spots: spots) + + let oldComponents: [Component] = self.controller.spots.map { $0.component } + + let changes = self.controller.generateChanges(from: newComponents, and: oldComponents) + XCTAssertEqual(changes.count, 1) + XCTAssertEqual(changes.first, .items) + + /// Test what changed on the items + let newItems = newComponents.first!.items + let oldItems = self.controller.spots.first!.items + var diff = Item.evaluate(newItems, oldModels: oldItems) + XCTAssertEqual(diff![0], .size) + XCTAssertEqual(diff![1], .size) + XCTAssertEqual(diff![2], .size) + XCTAssertEqual(diff![3], .size) + XCTAssertEqual(diff![4], .size) + XCTAssertEqual(diff![5], .size) + } + + func testReloadWithComponents() { + let initialComponents = [ + Component( + kind: "list", + items: [ + Item(title: "Fullname", subtitle: "Job title", kind: "image"), + Item(title: "Follow", kind: "toggle", meta: ["dynamic-height" : true]), + Item(title: "First name", subtitle: "Input first name",kind: "info"), + Item(title: "Last name", subtitle: "Input last name",kind: "info"), + Item(title: "Twitter", subtitle: "@twitter",kind: "info"), + Item(title: "", subtitle: "Biography", kind: "core", meta: ["dynamic-height" : true]) + ] + ) + ] + + let newComponents = [ + Component( + kind: "list", + items: [ + Item(title: "Fullname", subtitle: "Job title", text: "Bot", kind: "image"), + Item(title: "Follow", kind: "toggle", meta: ["dynamic-height" : true]), + Item(title: "First name", subtitle: "Input first name", text: "John", kind: "info"), + Item(title: "Last name", subtitle: "Input last name", text: "Hyperseed", kind: "info"), + Item(title: "Twitter", subtitle: "@johnhyperseed",kind: "info"), + Item(subtitle: "Biography", text: "John Hyperseed is a bot", kind: "core", meta: ["dynamic-height" : true]) + ] + ) + ] + + let spots = initialComponents.map { Factory.resolve(component: $0) } + + /// Validate setting up a controller + controller = Controller(spots: spots) + XCTAssertEqual(self.controller.spots.count, 1) + + /// Test first item in the first component of the first spot inside of the controller + XCTAssertEqual(self.controller.spots.first!.component.kind, spots.first!.component.kind) + XCTAssertEqual(self.controller.spots.first!.component.items[0].title, spots.first!.component.items[0].title) + XCTAssertEqual(self.controller.spots.first!.component.items[0].subtitle, spots.first!.component.items[0].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[0].kind, spots.first!.component.items[0].kind) + XCTAssertEqual(self.controller.spots.first!.component.items[0].size, spots.first!.component.items[0].size) + + XCTAssertTrue(initialComponents !== newComponents) + XCTAssertEqual(initialComponents.count, newComponents.count) + + var view: ListSpotCell? = self.controller.ui({ $0.kind == "image" }) + XCTAssertNil(view) + + controller.preloadView() + controller.viewDidAppear() + controller.spots.forEach { $0.render().layoutSubviews() } + + view = self.controller.ui({ $0.kind == "image" }) + XCTAssertNotNil(view) + + XCTAssertEqual(self.controller.spots.first!.component.items[0].title, initialComponents.first!.items[0].title) + XCTAssertEqual(self.controller.spots.first!.component.items[0].subtitle, initialComponents.first!.items[0].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[0].action, initialComponents.first!.items[0].action) + XCTAssertEqual(self.controller.spots.first!.component.items[0].kind, initialComponents.first!.items[0].kind) + XCTAssertNotEqual(self.controller.spots.first!.component.items[0].size, initialComponents.first!.items[0].size) + XCTAssertEqual(self.controller.spots.first!.component.items[0].size, CGSize(width: controller.view.frame.width, height: view!.preferredViewSize.height)) + XCTAssertEqual(self.controller.spots.first!.component.items[0].size, view!.frame.size) + + XCTAssertEqual(self.controller.spots.first!.component.items[1].title, initialComponents.first!.items[1].title) + XCTAssertEqual(self.controller.spots.first!.component.items[1].subtitle, initialComponents.first!.items[1].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[1].action, initialComponents.first!.items[1].action) + XCTAssertEqual(self.controller.spots.first!.component.items[1].kind, initialComponents.first!.items[1].kind) + XCTAssertNotEqual(self.controller.spots.first!.component.items[1].size, initialComponents.first!.items[1].size) + XCTAssertEqual(self.controller.spots.first!.component.items[1].size, CGSize(width: controller.view.frame.width, height: view!.preferredViewSize.height)) + XCTAssertEqual(self.controller.spots.first!.component.items[1].size, view!.frame.size) + + XCTAssertEqual(self.controller.spots.first!.component.items[2].title, initialComponents.first!.items[2].title) + XCTAssertEqual(self.controller.spots.first!.component.items[2].subtitle, initialComponents.first!.items[2].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[2].action, initialComponents.first!.items[2].action) + XCTAssertEqual(self.controller.spots.first!.component.items[2].kind, initialComponents.first!.items[2].kind) + XCTAssertNotEqual(self.controller.spots.first!.component.items[2].size, initialComponents.first!.items[2].size) + XCTAssertEqual(self.controller.spots.first!.component.items[2].size, CGSize(width: controller.view.frame.width, height: view!.preferredViewSize.height)) + XCTAssertEqual(self.controller.spots.first!.component.items[2].size, view!.frame.size) + + XCTAssertEqual(self.controller.spots.first!.component.items[3].title, initialComponents.first!.items[3].title) + XCTAssertEqual(self.controller.spots.first!.component.items[3].subtitle, initialComponents.first!.items[3].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[3].action, initialComponents.first!.items[3].action) + XCTAssertEqual(self.controller.spots.first!.component.items[3].kind, initialComponents.first!.items[3].kind) + XCTAssertNotEqual(self.controller.spots.first!.component.items[3].size, initialComponents.first!.items[3].size) + XCTAssertEqual(self.controller.spots.first!.component.items[3].size, CGSize(width: controller.view.frame.width, height: view!.preferredViewSize.height)) + XCTAssertEqual(self.controller.spots.first!.component.items[3].size, view!.frame.size) + + XCTAssertEqual(self.controller.spots.first!.component.items[4].title, initialComponents.first!.items[4].title) + XCTAssertEqual(self.controller.spots.first!.component.items[4].subtitle, initialComponents.first!.items[4].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[4].action, initialComponents.first!.items[4].action) + XCTAssertEqual(self.controller.spots.first!.component.items[4].kind, initialComponents.first!.items[4].kind) + XCTAssertNotEqual(self.controller.spots.first!.component.items[4].size, initialComponents.first!.items[4].size) + XCTAssertEqual(self.controller.spots.first!.component.items[4].size, CGSize(width: controller.view.frame.width, height: view!.preferredViewSize.height)) + XCTAssertEqual(self.controller.spots.first!.component.items[4].size, view!.frame.size) + + XCTAssertEqual(self.controller.spots.first!.component.items[5].title, initialComponents.first!.items[5].title) + XCTAssertEqual(self.controller.spots.first!.component.items[5].subtitle, initialComponents.first!.items[5].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[5].action, initialComponents.first!.items[5].action) + XCTAssertEqual(self.controller.spots.first!.component.items[5].kind, initialComponents.first!.items[5].kind) + XCTAssertNotEqual(self.controller.spots.first!.component.items[5].size, initialComponents.first!.items[5].size) + XCTAssertEqual(self.controller.spots.first!.component.items[5].size, CGSize(width: controller.view.frame.width, height: view!.preferredViewSize.height)) + XCTAssertEqual(self.controller.spots.first!.component.items[5].size, view!.frame.size) + + let exception = self.expectation(description: "Reload controller with components") + controller.reloadIfNeeded(newComponents) { + + XCTAssertEqual(self.controller.spots.first!.component.items[0].title, newComponents.first!.items[0].title) + XCTAssertEqual(self.controller.spots.first!.component.items[0].subtitle, newComponents.first!.items[0].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[0].action, newComponents.first!.items[0].action) + XCTAssertEqual(self.controller.spots.first!.component.items[0].kind, newComponents.first!.items[0].kind) + XCTAssertNotEqual(self.controller.spots.first!.component.items[0].size, newComponents.first!.items[0].size) + XCTAssertEqual(self.controller.spots.first!.component.items[0].size, CGSize(width: self.controller.view.frame.width, height: view!.preferredViewSize.height)) + XCTAssertEqual(self.controller.spots.first!.component.items[0].size, view!.frame.size) + + XCTAssertEqual(self.controller.spots.first!.component.items[1].title, newComponents.first!.items[1].title) + XCTAssertEqual(self.controller.spots.first!.component.items[1].subtitle, newComponents.first!.items[1].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[1].action, newComponents.first!.items[1].action) + XCTAssertEqual(self.controller.spots.first!.component.items[1].kind, newComponents.first!.items[1].kind) + XCTAssertNotEqual(self.controller.spots.first!.component.items[1].size, newComponents.first!.items[1].size) + XCTAssertEqual(self.controller.spots.first!.component.items[1].size, CGSize(width: self.controller.view.frame.width, height: view!.preferredViewSize.height)) + XCTAssertEqual(self.controller.spots.first!.component.items[1].size, view!.frame.size) + + XCTAssertEqual(self.controller.spots.first!.component.items[2].title, newComponents.first!.items[2].title) + XCTAssertEqual(self.controller.spots.first!.component.items[2].subtitle, newComponents.first!.items[2].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[2].action, newComponents.first!.items[2].action) + XCTAssertEqual(self.controller.spots.first!.component.items[2].kind, newComponents.first!.items[2].kind) + XCTAssertNotEqual(self.controller.spots.first!.component.items[2].size, newComponents.first!.items[2].size) + XCTAssertEqual(self.controller.spots.first!.component.items[2].size, CGSize(width: self.controller.view.frame.width, height: view!.preferredViewSize.height)) + XCTAssertEqual(self.controller.spots.first!.component.items[2].size, view!.frame.size) + + XCTAssertEqual(self.controller.spots.first!.component.items[3].title, newComponents.first!.items[3].title) + XCTAssertEqual(self.controller.spots.first!.component.items[3].subtitle, newComponents.first!.items[3].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[3].action, newComponents.first!.items[3].action) + XCTAssertEqual(self.controller.spots.first!.component.items[3].kind, newComponents.first!.items[3].kind) + XCTAssertNotEqual(self.controller.spots.first!.component.items[3].size, newComponents.first!.items[3].size) + XCTAssertEqual(self.controller.spots.first!.component.items[3].size, CGSize(width: self.controller.view.frame.width, height: view!.preferredViewSize.height)) + XCTAssertEqual(self.controller.spots.first!.component.items[3].size, view!.frame.size) + + XCTAssertEqual(self.controller.spots.first!.component.items[4].title, newComponents.first!.items[4].title) + XCTAssertEqual(self.controller.spots.first!.component.items[4].subtitle, newComponents.first!.items[4].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[4].action, newComponents.first!.items[4].action) + XCTAssertEqual(self.controller.spots.first!.component.items[4].kind, newComponents.first!.items[4].kind) + XCTAssertNotEqual(self.controller.spots.first!.component.items[4].size, newComponents.first!.items[4].size) + XCTAssertEqual(self.controller.spots.first!.component.items[4].size, CGSize(width: self.controller.view.frame.width, height: view!.preferredViewSize.height)) + XCTAssertEqual(self.controller.spots.first!.component.items[4].size, view!.frame.size) + + XCTAssertEqual(self.controller.spots.first!.component.items[5].title, newComponents.first!.items[5].title) + XCTAssertEqual(self.controller.spots.first!.component.items[5].subtitle, newComponents.first!.items[5].subtitle) + XCTAssertEqual(self.controller.spots.first!.component.items[5].action, newComponents.first!.items[5].action) + XCTAssertEqual(self.controller.spots.first!.component.items[5].kind, newComponents.first!.items[5].kind) + XCTAssertNotEqual(self.controller.spots.first!.component.items[5].size, newComponents.first!.items[5].size) + XCTAssertEqual(self.controller.spots.first!.component.items[5].size, CGSize(width: self.controller.view.frame.width, height: view!.preferredViewSize.height)) + XCTAssertEqual(self.controller.spots.first!.component.items[5].size, view!.frame.size) + + exception.fulfill() + } + waitForExpectations(timeout: 1.0, handler: nil) + } } diff --git a/SpotsTests/iOS/TestFactory.swift b/SpotsTests/iOS/TestFactory.swift index 589d4675..faae0962 100755 --- a/SpotsTests/iOS/TestFactory.swift +++ b/SpotsTests/iOS/TestFactory.swift @@ -1,6 +1,7 @@ @testable import Spots import Foundation import XCTest +import Brick class FactoryTests : XCTestCase { @@ -38,4 +39,33 @@ class FactoryTests : XCTestCase { XCTAssertTrue(spot.component == component) XCTAssertTrue(spot is GridSpot) } + + func testFactoryParsingComponents() { + let initialComponents = [ + Component( + kind: "list", + items: [ + Item(title: "Fullname", subtitle: "Job title", kind: "image"), + Item(title: "Follow", kind: "toggle", meta: ["dynamic-height" : true]), + Item(title: "First name", subtitle: "Input first name",kind: "info"), + Item(title: "Last name", subtitle: "Input last name",kind: "info"), + Item(title: "Twitter", subtitle: "@twitter",kind: "info"), + Item(title: "", subtitle: "Biography", kind: "core", meta: ["dynamic-height" : true]) + ] + ) + ] + + let spots = initialComponents.map { Factory.resolve(component: $0) } + + /// Validate factory process + XCTAssertEqual(spots.count, 1) + XCTAssert(spots.first is ListSpot) + + /// Test first item in the first component of the first spot + XCTAssertEqual(spots.first!.component.kind, "list") + XCTAssertEqual(spots.first!.component.items[0].title, "Fullname") + XCTAssertEqual(spots.first!.component.items[0].subtitle, "Job title") + XCTAssertEqual(spots.first!.component.items[0].kind, "image") + XCTAssertEqual(spots.first!.component.items[0].size, CGSize(width: UIScreen.main.bounds.width, height: 44)) + } } diff --git a/SpotsTests/iOS/TestSpotsScrollView.swift b/SpotsTests/iOS/TestSpotsScrollView.swift index 146b52aa..3eb04758 100644 --- a/SpotsTests/iOS/TestSpotsScrollView.swift +++ b/SpotsTests/iOS/TestSpotsScrollView.swift @@ -9,6 +9,11 @@ extension Controller { let _ = view } + func viewDidAppear() { + viewWillAppear(true) + viewDidAppear(true) + } + func scrollTo(_ point: CGPoint) { scrollView.setContentOffset(point, animated: false) scrollView.layoutSubviews()