Slide 1

Slide 1 text

3೥Ҏ্ӡ༻͍ͯ͠ΔΞϓϦʹUIςετΛಋೖͨ͠ iOSDC Japan 2017 Զίϯ Vol.1 / Day. 2 1

Slide 2

Slide 2 text

Profile Kazuya Ueoka Timers inc.ͷiOSΤϯδχΞ Twitter: @fromkk Github: fromkk Qiita: fromkk 2

Slide 3

Slide 3 text

3

Slide 4

Slide 4 text

4

Slide 5

Slide 5 text

5

Slide 6

Slide 6 text

ΤϯδχΞืूத 6

Slide 7

Slide 7 text

ࠓճUIςετΛಋೖͨ͠ΞϓϦ 7

Slide 8

Slide 8 text

8

Slide 9

Slide 9 text

Famm • 2014/05ϦϦʔεͷՈ଒޲͚ΫϩʔζυSNSΞϓϦ • iOS/AndroidରԠ • ຖ݄ϑΥτΧϨϯμʔΛҹ࡮ͯ͠Ϣʔβʔʹಧ͚Δ 9

Slide 10

Slide 10 text

UIςετৼΓฦΓ 10

Slide 11

Slide 11 text

WWDC 2015 • UIཁૉΛݕࡧɾΞΫγϣϯΛ࣮ߦ • UIͷϓϩύςΟͱঢ়ଶΛݕূ • UI recording • ςετ݁ՌΛϨϙʔτ 11

Slide 12

Slide 12 text

ʊਓਓਓਓਓਓਓਓਓਓਓਓਓਓਓʊ ʼɹԿ͍͍͔ͯ͠෼͔Βͳ͍ ɹʻ ʉY^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Yʉ 12

Slide 13

Slide 13 text

UIςετͷਏΈ • ͍͖ͳΓॳظը໘͕ىಈ͢Δ • ຖճཁૉΛݕࡧ͢Δͷ͕໘౗ • ஗͍ 13

Slide 14

Slide 14 text

UIςετΛಋೖ͢Δʹ౰ͨͬͯఘΊͨࣄ 14

Slide 15

Slide 15 text

طଘը໘ͷςετΛॻ͘ࣄ͸ఘΊͨ • طଘίʔυ͕ີ݁߹ա͗ͨ(ViewController಺Ͱ௚઀Ϟσϧ Λݺͼग़ͨ͠Γ௚઀URLʹΞΫηεͨ͠Γ...) • ৽نը໘ɾཁૉʹ͸ accessibilityIdentifier Λؤுͬ ͯৼΔ༷ʹҙࣝ 15

Slide 16

Slide 16 text

΍ͬͨࣄ ͦͷ1 16

Slide 17

Slide 17 text

ΞϓϦىಈ࣌ʹจࣈྻͰViewControllerΛ ࢦఆͯ͠ىಈग़དྷΔ༷ʹͨ͠ • iOSDC 2016ͷ @dealforest ͞ΜͷൃදΛࢀߟ 17

Slide 18

Slide 18 text

ॳظը໘ΛStoryboard͔Β։͘ઃఆΛ࡟আ ↓ 18

Slide 19

Slide 19 text

ViewController༻ͱΦϓγϣϯ༻ͷΩʔΛܾΊ͓ͯ͘ struct LaunchKeys { static let viewController: String = "LAUNCH_VIEW_CONTROLLER" static let userInfo: String = "LAUNCH_USER_INFO" } 19

Slide 20

Slide 20 text

εΩʔϜΛฤू ઌ΄ͲܾΊͨΩʔʹରͯ͠஋Λઃఆ͢Δ 20

Slide 21

Slide 21 text

Creatable protocol protocol Creatable { func create(_ identifier: String, userInfo: [AnyHashable: Any]?) -> T? } 21

Slide 22

Slide 22 text

Adopt to Creatable struct Creator: Creatable { func create(_ identifier: String, userInfo: [AnyHashable : Any]?) -> T? { switch identifier { case "ViewController": return viewController() as? T default: return nil } } } extension Creator { func viewController() -> ViewController { let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main) return storyboard.instantiateInitialViewController() as! ViewController } } 22

Slide 23

Slide 23 text

Launcher struct Launcher { var creator: Creatable init(with creator: Creatable) { self.creator = creator } func launch() -> T? { guard let viewControllerName: String = ProcessInfo.processInfo.environment[LaunchKeys.viewController] else { return nil } var userInfo: [AnyHashable: Any]? = nil if let userInfoString: String = ProcessInfo.processInfo.environment[LaunchKeys.userInfo], let userInfoData: Data = userInfoString.data(using: .utf8) { userInfo = (try? JSONSerialization.jsonObject(with: userInfoData, options: [])) as? [AnyHashable : Any] } return creator.create(viewControllerName, userInfo: userInfo) } } 23

Slide 24

Slide 24 text

AppDelegate.swift let creator = Creator() let rootViewController: UIViewController #if DEBUG if let viewController: UIViewController = Launcher(with: creator).launch() { rootViewController = viewController } else { rootViewController = creator.rootViewController() } #else rootViewController = creator.rootViewController() #endif window?.rootViewController = rootViewController 24

Slide 25

Slide 25 text

΍ͬͨࣄ ͦͷ2 25

Slide 26

Slide 26 text

UIςετ͔ΒViewControllerΛࢦఆͯ͠ ςετग़དྷΔ༷ʹͨ͠ 26

Slide 27

Slide 27 text

ςετଆͷλʔήοτʹ΋ Launcher Λ࡞੒ 27

Slide 28

Slide 28 text

import XCTest struct Launcher { var viewControllerName: String var userInfo: [AnyHashable: Any]? init(viewControllerName: String, userInfo: [AnyHashable: Any]? = nil) { self.viewControllerName = viewControllerName self.userInfo = userInfo } var env: [String: String] { var result: [String: String] = [LaunchKeys.viewController: self.viewControllerName] if let userInfo: [AnyHashable: Any] = self.userInfo { if let data: Data = try? JSONSerialization.data(withJSONObject: userInfo, options: []), let userInfoString: String = String(data: data, encoding: .utf8) { result[LaunchKeys.userInfo] = userInfoString } } return result } func launch() -> XCUIApplication { let app: XCUIApplication = XCUIApplication() app.launchEnvironment = env app.launch() return app } } 28

Slide 29

Slide 29 text

΍ͬͨࣄ ͦͷ3 29

Slide 30

Slide 30 text

ϖʔδ಺ͷཁૉΛ੔ཧ • σβΠφʔ͔ΒZeplinͱ͍͏πʔϧͰ UIΛ໯͏ • UIΛ໯ͬͨஈ֊ͰཁૉΛચ͍ग़͠ • ςετλʔήοτͰཁૉϦετΛ࡞੒ ʢPage Object Design Patternʣ 30

Slide 31

Slide 31 text

PageObjectsRepresentable protocol protocol PageObjectsRepresentable { var app: XCUIApplication init(app: XCUIApplication) } 31

Slide 32

Slide 32 text

Adopt to PageObjectsRepresentable struct ViewControllerPage: PageObjectsRepresentable { var app: XCUIApplication init(app: XCUIApplication) { self.app = app } var label: XCUIElement { return app.staticTexts["label"] } } 32

Slide 33

Slide 33 text

Test import XCTest class LunchViewControllerTests: XCTestCase { var viewControllerName: String { return "ViewController" } func testLunchLabel() { let launcher = Launcher(viewControllerName: viewControllerName) let app = launcher.launch() let page = ViewControllerPage(app: app) XCTAssertTrue(page.label.exists) XCTAssertEqual(page.label.label, "Lunch") } } 33

Slide 34

Slide 34 text

͋Εʁ͜ΕϥΠϒϥϦԽ ग़དྷΔΜ͡Όͳ͍ʁ 34

Slide 35

Slide 35 text

https://github.com/ fromkk/Lunch 35

Slide 36

Slide 36 text

Lunch • Launcher • Creatable • LaunchKeys • PageObjectsRepresentable • ViewControllerTestable ͷΈఆٛ 36

Slide 37

Slide 37 text

! ͍ͩ͘͞ 37

Slide 38

Slide 38 text

Ԡ༻ฤ 38

Slide 39

Slide 39 text

UIςετͰϞοΫͷ஋Λ൓өͤͯ͞ςετ 39

Slide 40

Slide 40 text

ద౰ͳϞσϧΛఆٛ struct HogeModel { var hoge: String } 40

Slide 41

Slide 41 text

ViewControllerʹϞσϧΛอ࣋ class HogeViewController: UIViewController { @IBOutlet weak var hogeLabel: UILabel! var hogeModel: HogeModel { didSet { hogeLabel.text = hogeModel.hoge } } } 41

Slide 42

Slide 42 text

CreatorΛ֦ு struct Creator: Creatable { func create(_ identifier: String, userInfo: [AnyHashable : Any]?) -> T? { switch identifier { case "HogeViewController": guard let data = (userInfo?["MOCK_JSON"] as? String)?.data(using: .utf8), let json: [String: String] = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: String], let hoge = json["hoge"] else { return nil } let model = HogeModel(hoge: hoge) return hogeViewController(with: model) as? T default: return nil } } } extension Creator { func hogeViewController(with hoge: HogeModel) -> HogeViewController { let viewController = HogeViewController() viewController.hogeModel = hoge return viewController } } 42

Slide 43

Slide 43 text

UIςετ͔ΒJSONจࣈྻΛΞϓϦʹ౉ͯ͠ςετ func testModel() { let launcher = Launcher(viewControllerName: viewControllerName, userInfo: ["MOCK_JSON": "{\"hoge\": \"fuga\"}"]) let app = launcher.launch() let page = HogeViewControllerPage(app: app) XCTAssertTrue(page.hogeLabel.exists) XCTAssertEqual("fuga", page.hogeLabel.label) } 43

Slide 44

Slide 44 text

! 44

Slide 45

Slide 45 text

ͦͷଞͷςΟοϓε 45

Slide 46

Slide 46 text

UIͷ ঢ়ଶ Λςετ͢Δ • ཁૉ Λઃఆ͢Δͷ͸ accessibilityIdentifier • ঢ়ଶ Λઃఆ͢Δͷ͸ accessibilityValue • छྨ Λઃఆ͢Δͷ͸ accessibilityTraits • UIςετଆͰ͸ͦΕͧΕ identifier , value, elementType ͰΞΫηεग़དྷΔ 46

Slide 47

Slide 47 text

CIͰ͸UIςετ͸࣮ߦ͠ͳ͍༷ʹ • CI؀ڥ(Bitrise)ͰUIςετΛ࣮ߦ͢Δͱֻ͕͔࣌ؒΓ͗͢ Δ • xcodebuild test ʹ -only-testing:$UITEST_SCHEME Λ௥Ճ͢ΔࣄͰςετ͢ΔεΩʔϜΛબ୒ • ಉ༷ʹ -skip-testing:$UITEST_SCHEME Ͱಛఆͷςε τͷεΩʔϜΛεΩοϓ͢Δࣄ͕ग़དྷΔ 47

Slide 48

Slide 48 text

ϩʔΧϥΠζରԠ • ϩέʔϧ৘ใΛมߋ͢Δʹ͸ XCUIApplication ͷ launchArguments Λมߋ • ྫ: ["-AppleLanguages", "(ja)", "- AppleLocale", "ja-JP"] Lunch ͷ৔߹ launcher(targetViewController: self, locale: "ja-JP") ͷ༷ʹࢦఆՄೳ 48

Slide 49

Slide 49 text

UIςετΛಋೖͯ͠Έͯ • Ұؾʹશ෦Λςετ͢Δࣄ͸΍͸Γ೉͍͠ • ग़དྷΔࣄ͔Βগͣͭ͠ " • ͕͜͜ಈ࡞͠ͳ͍ͱΞϓϦͱͯ͠੒Γཱͨͳ͍ͱ͍͏࠷௿ݶͷ ॴ͔ΒςετΛॻ͍ͯߦ͘ͷ͕ྑͦ͞͏ • ϢχοτςετͰ΋ςετग़དྷΔՕॴ͸ͳΔ΂͘Ϣχοτςε τΛॻ͍ͨํ͕͍͍͔΋ $ 49

Slide 50

Slide 50 text

Summary • LunchͰUIςετ΁ͷϋʔυϧ͕গ͠Լ͕Γ·ͨ͠ • UIςετͰཁૉΛڞ௨Խ͢ΔࣄͰ࢖͍ճ͕͠؆୯ • ಛఆͷViewController͚ͩ։͖͍ͨ࣌ʹεΩʔϜฤू͚ͩͰ ָνϯ # • ෭࣍తʹΠϯελϯεԽ͢ΔॲཧΛڞ௨Խग़དྷͨ 50

Slide 51

Slide 51 text

! ͍ͩ͘͞ https://github.com/fromkk/Lunch 51

Slide 52

Slide 52 text

͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠ 52