├── .gitignore ├── Account.swift ├── LICENSE.md ├── README.md ├── Session.swift ├── SwiftTalk.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ │ └── Package.resolved │ └── xcuserdata │ │ ├── chris.xcuserdatad │ │ └── UserInterfaceState.xcuserstate │ │ └── florian.xcuserdatad │ │ └── UserInterfaceState.xcuserstate └── xcuserdata │ ├── chris.xcuserdatad │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ └── xcschemes │ │ └── xcschememanagement.plist │ └── florian.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── SwiftTalk ├── AllEpisodes.swift ├── AppDelegate.swift ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── AuthenticationHelpers.swift ├── Base.lproj └── LaunchScreen.storyboard ├── Helpers.swift ├── Info.plist ├── Model ├── Account.swift ├── EpisodeProgress.swift ├── Model.swift ├── Session.swift └── Store.swift ├── Preview Content └── Preview Assets.xcassets │ └── Contents.json ├── Resource.swift ├── SceneDelegate.swift ├── ViewHelpers.swift ├── Views ├── AllEpisodes.swift ├── CollectionDetails.swift ├── CollectionsList.swift ├── ContentView.swift ├── Episode.swift ├── EpisodeDetail.swift ├── Loader.swift ├── Player.swift └── PlayerView.swift ├── collections.json └── episodes.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /Account.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Account.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 18.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct WindowKey: EnvironmentKey { 12 | static let defaultValue: UIWindow? = nil 13 | } 14 | 15 | extension EnvironmentValues { 16 | var window: UIWindow? { 17 | get { 18 | self[WindowKey.self] 19 | } 20 | set { 21 | self[WindowKey.self] = newValue 22 | } 23 | } 24 | } 25 | 26 | struct Account: View { 27 | @Environment(\.window) var window 28 | @ObjectBinding var session = Session.shared 29 | var body: some View { 30 | Form { 31 | if session.credentials == nil { 32 | Button(action: { 33 | getAuthToken(window: self.window!, onComplete: { result in 34 | switch result { 35 | case let .failure(e): print(e) // todo 36 | case let .success(info): 37 | self.session.credentials = info 38 | } 39 | }) 40 | }) { 41 | Text("Log In") 42 | } 43 | } else { 44 | Button(action: { self.session.credentials = nil }) { 45 | Text("Log Out") 46 | } 47 | } 48 | }.navigationBarTitle("Account") 49 | } 50 | } 51 | 52 | #if DEBUG 53 | struct Account_Previews: PreviewProvider { 54 | static var previews: some View { 55 | Account() 56 | } 57 | } 58 | #endif 59 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 objc.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift Talk App 2 | 3 | We're building a Swift Talk app using SwiftUI. Check out this Swit Talk collection for all the accompanying episodes: http://talk.objc.io/collections/swiftui 4 | -------------------------------------------------------------------------------- /Session.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Session.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 18.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import KeychainItem 11 | import SwiftUI 12 | import Combine 13 | 14 | final class Session: BindableObject { 15 | @KeychainItem(account: "sessionId") private var sessionId 16 | @KeychainItem(account: "csrf") private var csrf 17 | 18 | let willChange = PassthroughSubject<(), Never>() 19 | 20 | var credentials: (sessionId: String, csrf: String)? { 21 | get { 22 | guard let s = sessionId, let c = csrf else { return nil } 23 | return (s, c) 24 | } 25 | set { 26 | willChange.send() 27 | sessionId = newValue?.sessionId 28 | csrf = newValue?.csrf 29 | } 30 | } 31 | 32 | static let shared = Session() 33 | } 34 | -------------------------------------------------------------------------------- /SwiftTalk.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 170307D322CB8EB500958FFB /* ViewHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170307D222CB8EB500958FFB /* ViewHelpers.swift */; }; 11 | 170307D522CB8F7A00958FFB /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 170307D422CB8F7A00958FFB /* Helpers.swift */; }; 12 | 1797835322F0407E002E8711 /* AuthenticationHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1797835222F0407E002E8711 /* AuthenticationHelpers.swift */; }; 13 | 1797835622F04116002E8711 /* EpisodeProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1797835422F04116002E8711 /* EpisodeProgress.swift */; }; 14 | 1797835722F04116002E8711 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1797835522F04116002E8711 /* Store.swift */; }; 15 | 8325D5DC22E098380087B423 /* KeychainItem in Frameworks */ = {isa = PBXBuildFile; productRef = 8325D5DB22E098380087B423 /* KeychainItem */; }; 16 | 83315E0822F89293008BC6B1 /* Loader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83315E0722F89293008BC6B1 /* Loader.swift */; }; 17 | 8361B42D22EEDEF700ABBC59 /* Session.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8361B42B22EEDEF700ABBC59 /* Session.swift */; }; 18 | 8361B42E22EEDEF700ABBC59 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8361B42C22EEDEF700ABBC59 /* Account.swift */; }; 19 | 8363E0E422C4C59E00B55A7B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8363E0E322C4C59E00B55A7B /* AppDelegate.swift */; }; 20 | 8363E0E622C4C59E00B55A7B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8363E0E522C4C59E00B55A7B /* SceneDelegate.swift */; }; 21 | 8363E0E822C4C59E00B55A7B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8363E0E722C4C59E00B55A7B /* ContentView.swift */; }; 22 | 8363E0EA22C4C59F00B55A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8363E0E922C4C59F00B55A7B /* Assets.xcassets */; }; 23 | 8363E0ED22C4C59F00B55A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8363E0EC22C4C59F00B55A7B /* Preview Assets.xcassets */; }; 24 | 8363E0F022C4C59F00B55A7B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8363E0EE22C4C59F00B55A7B /* LaunchScreen.storyboard */; }; 25 | 8363E0F922C4C7FE00B55A7B /* TinyNetworking in Frameworks */ = {isa = PBXBuildFile; productRef = 8363E0F822C4C7FE00B55A7B /* TinyNetworking */; }; 26 | 8363E0FC22C4C83F00B55A7B /* Model in Frameworks */ = {isa = PBXBuildFile; productRef = 8363E0FB22C4C83F00B55A7B /* Model */; }; 27 | 8363E0FE22C4C83F00B55A7B /* ViewHelpers in Frameworks */ = {isa = PBXBuildFile; productRef = 8363E0FD22C4C83F00B55A7B /* ViewHelpers */; }; 28 | 8363E10222C4C86D00B55A7B /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8363E0FF22C4C86D00B55A7B /* Resource.swift */; }; 29 | 8363E10322C4C86D00B55A7B /* episodes.json in Resources */ = {isa = PBXBuildFile; fileRef = 8363E10022C4C86D00B55A7B /* episodes.json */; }; 30 | 8363E10422C4C86D00B55A7B /* collections.json in Resources */ = {isa = PBXBuildFile; fileRef = 8363E10122C4C86D00B55A7B /* collections.json */; }; 31 | 8363E10622C4C88900B55A7B /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8363E10522C4C88900B55A7B /* Model.swift */; }; 32 | 8363E10822C4C97F00B55A7B /* CollectionsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8363E10722C4C97F00B55A7B /* CollectionsList.swift */; }; 33 | 8363E10A22C4CB8700B55A7B /* CollectionDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8363E10922C4CB8700B55A7B /* CollectionDetails.swift */; }; 34 | 83758B7B22CE0FCC00C7978D /* AllEpisodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83758B7A22CE0FCC00C7978D /* AllEpisodes.swift */; }; 35 | 83758B8122CE459100C7978D /* Episode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83758B8022CE459100C7978D /* Episode.swift */; }; 36 | 83758B8322CE465500C7978D /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83758B8222CE465500C7978D /* Player.swift */; }; 37 | /* End PBXBuildFile section */ 38 | 39 | /* Begin PBXFileReference section */ 40 | 170307D222CB8EB500958FFB /* ViewHelpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewHelpers.swift; sourceTree = ""; }; 41 | 170307D422CB8F7A00958FFB /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; 42 | 1797835222F0407E002E8711 /* AuthenticationHelpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationHelpers.swift; sourceTree = ""; }; 43 | 1797835422F04116002E8711 /* EpisodeProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodeProgress.swift; sourceTree = ""; }; 44 | 1797835522F04116002E8711 /* Store.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 45 | 83315E0722F89293008BC6B1 /* Loader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Loader.swift; sourceTree = ""; }; 46 | 8361B42B22EEDEF700ABBC59 /* Session.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Session.swift; sourceTree = ""; }; 47 | 8361B42C22EEDEF700ABBC59 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; 48 | 8363E0E022C4C59E00B55A7B /* SwiftTalk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftTalk.app; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | 8363E0E322C4C59E00B55A7B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 50 | 8363E0E522C4C59E00B55A7B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 51 | 8363E0E722C4C59E00B55A7B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 52 | 8363E0E922C4C59F00B55A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 53 | 8363E0EC22C4C59F00B55A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 54 | 8363E0EF22C4C59F00B55A7B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 55 | 8363E0F122C4C59F00B55A7B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 56 | 8363E0FF22C4C86D00B55A7B /* Resource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resource.swift; sourceTree = ""; }; 57 | 8363E10022C4C86D00B55A7B /* episodes.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = episodes.json; sourceTree = ""; }; 58 | 8363E10122C4C86D00B55A7B /* collections.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = collections.json; sourceTree = ""; }; 59 | 8363E10522C4C88900B55A7B /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; 60 | 8363E10722C4C97F00B55A7B /* CollectionsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionsList.swift; sourceTree = ""; }; 61 | 8363E10922C4CB8700B55A7B /* CollectionDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionDetails.swift; sourceTree = ""; }; 62 | 83758B7A22CE0FCC00C7978D /* AllEpisodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AllEpisodes.swift; path = SwiftTalk/AllEpisodes.swift; sourceTree = SOURCE_ROOT; }; 63 | 83758B8022CE459100C7978D /* Episode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Episode.swift; sourceTree = ""; }; 64 | 83758B8222CE465500C7978D /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = ""; }; 65 | /* End PBXFileReference section */ 66 | 67 | /* Begin PBXFrameworksBuildPhase section */ 68 | 8363E0DD22C4C59E00B55A7B /* Frameworks */ = { 69 | isa = PBXFrameworksBuildPhase; 70 | buildActionMask = 2147483647; 71 | files = ( 72 | 8363E0FE22C4C83F00B55A7B /* ViewHelpers in Frameworks */, 73 | 8363E0FC22C4C83F00B55A7B /* Model in Frameworks */, 74 | 8363E0F922C4C7FE00B55A7B /* TinyNetworking in Frameworks */, 75 | 8325D5DC22E098380087B423 /* KeychainItem in Frameworks */, 76 | ); 77 | runOnlyForDeploymentPostprocessing = 0; 78 | }; 79 | /* End PBXFrameworksBuildPhase section */ 80 | 81 | /* Begin PBXGroup section */ 82 | 17CD7D7A22CB9BAA00167A6A /* Views */ = { 83 | isa = PBXGroup; 84 | children = ( 85 | 83315E0722F89293008BC6B1 /* Loader.swift */, 86 | 83758B8022CE459100C7978D /* Episode.swift */, 87 | 83758B8222CE465500C7978D /* Player.swift */, 88 | 83758B7A22CE0FCC00C7978D /* AllEpisodes.swift */, 89 | 8363E0E722C4C59E00B55A7B /* ContentView.swift */, 90 | 8363E10922C4CB8700B55A7B /* CollectionDetails.swift */, 91 | 8363E10722C4C97F00B55A7B /* CollectionsList.swift */, 92 | ); 93 | path = Views; 94 | sourceTree = ""; 95 | }; 96 | 834FEF9822F062A30062CF6E /* Frameworks */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | ); 100 | name = Frameworks; 101 | sourceTree = ""; 102 | }; 103 | 8361B42F22EEDF0700ABBC59 /* Model */ = { 104 | isa = PBXGroup; 105 | children = ( 106 | 8363E10522C4C88900B55A7B /* Model.swift */, 107 | 8361B42B22EEDEF700ABBC59 /* Session.swift */, 108 | 8361B42C22EEDEF700ABBC59 /* Account.swift */, 109 | 1797835422F04116002E8711 /* EpisodeProgress.swift */, 110 | 1797835522F04116002E8711 /* Store.swift */, 111 | ); 112 | path = Model; 113 | sourceTree = ""; 114 | }; 115 | 8363E0D722C4C59E00B55A7B = { 116 | isa = PBXGroup; 117 | children = ( 118 | 8363E0E222C4C59E00B55A7B /* SwiftTalk */, 119 | 8363E0E122C4C59E00B55A7B /* Products */, 120 | 834FEF9822F062A30062CF6E /* Frameworks */, 121 | ); 122 | sourceTree = ""; 123 | }; 124 | 8363E0E122C4C59E00B55A7B /* Products */ = { 125 | isa = PBXGroup; 126 | children = ( 127 | 8363E0E022C4C59E00B55A7B /* SwiftTalk.app */, 128 | ); 129 | name = Products; 130 | sourceTree = ""; 131 | }; 132 | 8363E0E222C4C59E00B55A7B /* SwiftTalk */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 8363E10022C4C86D00B55A7B /* episodes.json */, 136 | 8363E10122C4C86D00B55A7B /* collections.json */, 137 | 8363E0FF22C4C86D00B55A7B /* Resource.swift */, 138 | 8363E0E322C4C59E00B55A7B /* AppDelegate.swift */, 139 | 8363E0E522C4C59E00B55A7B /* SceneDelegate.swift */, 140 | 170307D422CB8F7A00958FFB /* Helpers.swift */, 141 | 8363E0E922C4C59F00B55A7B /* Assets.xcassets */, 142 | 8363E0EE22C4C59F00B55A7B /* LaunchScreen.storyboard */, 143 | 8363E0F122C4C59F00B55A7B /* Info.plist */, 144 | 8363E0EB22C4C59F00B55A7B /* Preview Content */, 145 | 8361B42F22EEDF0700ABBC59 /* Model */, 146 | 17CD7D7A22CB9BAA00167A6A /* Views */, 147 | 170307D222CB8EB500958FFB /* ViewHelpers.swift */, 148 | 1797835222F0407E002E8711 /* AuthenticationHelpers.swift */, 149 | ); 150 | path = SwiftTalk; 151 | sourceTree = ""; 152 | }; 153 | 8363E0EB22C4C59F00B55A7B /* Preview Content */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | 8363E0EC22C4C59F00B55A7B /* Preview Assets.xcassets */, 157 | ); 158 | path = "Preview Content"; 159 | sourceTree = ""; 160 | }; 161 | /* End PBXGroup section */ 162 | 163 | /* Begin PBXNativeTarget section */ 164 | 8363E0DF22C4C59E00B55A7B /* SwiftTalk */ = { 165 | isa = PBXNativeTarget; 166 | buildConfigurationList = 8363E0F422C4C59F00B55A7B /* Build configuration list for PBXNativeTarget "SwiftTalk" */; 167 | buildPhases = ( 168 | 8363E0DC22C4C59E00B55A7B /* Sources */, 169 | 8363E0DD22C4C59E00B55A7B /* Frameworks */, 170 | 8363E0DE22C4C59E00B55A7B /* Resources */, 171 | ); 172 | buildRules = ( 173 | ); 174 | dependencies = ( 175 | ); 176 | name = SwiftTalk; 177 | packageProductDependencies = ( 178 | 8363E0F822C4C7FE00B55A7B /* TinyNetworking */, 179 | 8363E0FB22C4C83F00B55A7B /* Model */, 180 | 8363E0FD22C4C83F00B55A7B /* ViewHelpers */, 181 | 8325D5DB22E098380087B423 /* KeychainItem */, 182 | ); 183 | productName = SwiftTalk; 184 | productReference = 8363E0E022C4C59E00B55A7B /* SwiftTalk.app */; 185 | productType = "com.apple.product-type.application"; 186 | }; 187 | /* End PBXNativeTarget section */ 188 | 189 | /* Begin PBXProject section */ 190 | 8363E0D822C4C59E00B55A7B /* Project object */ = { 191 | isa = PBXProject; 192 | attributes = { 193 | LastSwiftUpdateCheck = 1100; 194 | LastUpgradeCheck = 1100; 195 | ORGANIZATIONNAME = "Chris Eidhof"; 196 | TargetAttributes = { 197 | 8363E0DF22C4C59E00B55A7B = { 198 | CreatedOnToolsVersion = 11.0; 199 | }; 200 | }; 201 | }; 202 | buildConfigurationList = 8363E0DB22C4C59E00B55A7B /* Build configuration list for PBXProject "SwiftTalk" */; 203 | compatibilityVersion = "Xcode 9.3"; 204 | developmentRegion = en; 205 | hasScannedForEncodings = 0; 206 | knownRegions = ( 207 | en, 208 | Base, 209 | ); 210 | mainGroup = 8363E0D722C4C59E00B55A7B; 211 | packageReferences = ( 212 | 8363E0F722C4C7FE00B55A7B /* XCRemoteSwiftPackageReference "tiny-networking" */, 213 | 8363E0FA22C4C83F00B55A7B /* XCRemoteSwiftPackageReference "swift-talk-shared" */, 214 | 8325D5DA22E098380087B423 /* XCRemoteSwiftPackageReference "keychain-item" */, 215 | ); 216 | productRefGroup = 8363E0E122C4C59E00B55A7B /* Products */; 217 | projectDirPath = ""; 218 | projectRoot = ""; 219 | targets = ( 220 | 8363E0DF22C4C59E00B55A7B /* SwiftTalk */, 221 | ); 222 | }; 223 | /* End PBXProject section */ 224 | 225 | /* Begin PBXResourcesBuildPhase section */ 226 | 8363E0DE22C4C59E00B55A7B /* Resources */ = { 227 | isa = PBXResourcesBuildPhase; 228 | buildActionMask = 2147483647; 229 | files = ( 230 | 8363E0F022C4C59F00B55A7B /* LaunchScreen.storyboard in Resources */, 231 | 8363E10322C4C86D00B55A7B /* episodes.json in Resources */, 232 | 8363E0ED22C4C59F00B55A7B /* Preview Assets.xcassets in Resources */, 233 | 8363E10422C4C86D00B55A7B /* collections.json in Resources */, 234 | 8363E0EA22C4C59F00B55A7B /* Assets.xcassets in Resources */, 235 | ); 236 | runOnlyForDeploymentPostprocessing = 0; 237 | }; 238 | /* End PBXResourcesBuildPhase section */ 239 | 240 | /* Begin PBXSourcesBuildPhase section */ 241 | 8363E0DC22C4C59E00B55A7B /* Sources */ = { 242 | isa = PBXSourcesBuildPhase; 243 | buildActionMask = 2147483647; 244 | files = ( 245 | 8363E10622C4C88900B55A7B /* Model.swift in Sources */, 246 | 8363E10822C4C97F00B55A7B /* CollectionsList.swift in Sources */, 247 | 170307D322CB8EB500958FFB /* ViewHelpers.swift in Sources */, 248 | 8363E0E422C4C59E00B55A7B /* AppDelegate.swift in Sources */, 249 | 83758B8322CE465500C7978D /* Player.swift in Sources */, 250 | 170307D522CB8F7A00958FFB /* Helpers.swift in Sources */, 251 | 8361B43322EEDF7100ABBC59 /* EpisodeProgress.swift in Sources */, 252 | 83315E0822F89293008BC6B1 /* Loader.swift in Sources */, 253 | 83758B8122CE459100C7978D /* Episode.swift in Sources */, 254 | 1797835322F0407E002E8711 /* AuthenticationHelpers.swift in Sources */, 255 | 83758B7B22CE0FCC00C7978D /* AllEpisodes.swift in Sources */, 256 | 8363E10222C4C86D00B55A7B /* Resource.swift in Sources */, 257 | 8363E0E622C4C59E00B55A7B /* SceneDelegate.swift in Sources */, 258 | 8361B42E22EEDEF700ABBC59 /* Account.swift in Sources */, 259 | 1797835722F04116002E8711 /* Store.swift in Sources */, 260 | 1797835622F04116002E8711 /* EpisodeProgress.swift in Sources */, 261 | 8363E10A22C4CB8700B55A7B /* CollectionDetails.swift in Sources */, 262 | 8361B42D22EEDEF700ABBC59 /* Session.swift in Sources */, 263 | 8363E0E822C4C59E00B55A7B /* ContentView.swift in Sources */, 264 | ); 265 | runOnlyForDeploymentPostprocessing = 0; 266 | }; 267 | /* End PBXSourcesBuildPhase section */ 268 | 269 | /* Begin PBXVariantGroup section */ 270 | 8363E0EE22C4C59F00B55A7B /* LaunchScreen.storyboard */ = { 271 | isa = PBXVariantGroup; 272 | children = ( 273 | 8363E0EF22C4C59F00B55A7B /* Base */, 274 | ); 275 | name = LaunchScreen.storyboard; 276 | sourceTree = ""; 277 | }; 278 | /* End PBXVariantGroup section */ 279 | 280 | /* Begin XCBuildConfiguration section */ 281 | 8363E0F222C4C59F00B55A7B /* Debug */ = { 282 | isa = XCBuildConfiguration; 283 | buildSettings = { 284 | ALWAYS_SEARCH_USER_PATHS = NO; 285 | CLANG_ANALYZER_NONNULL = YES; 286 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 287 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 288 | CLANG_CXX_LIBRARY = "libc++"; 289 | CLANG_ENABLE_MODULES = YES; 290 | CLANG_ENABLE_OBJC_ARC = YES; 291 | CLANG_ENABLE_OBJC_WEAK = YES; 292 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 293 | CLANG_WARN_BOOL_CONVERSION = YES; 294 | CLANG_WARN_COMMA = YES; 295 | CLANG_WARN_CONSTANT_CONVERSION = YES; 296 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 298 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 299 | CLANG_WARN_EMPTY_BODY = YES; 300 | CLANG_WARN_ENUM_CONVERSION = YES; 301 | CLANG_WARN_INFINITE_RECURSION = YES; 302 | CLANG_WARN_INT_CONVERSION = YES; 303 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 304 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 305 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 306 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 307 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 308 | CLANG_WARN_STRICT_PROTOTYPES = YES; 309 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 310 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 311 | CLANG_WARN_UNREACHABLE_CODE = YES; 312 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 313 | COPY_PHASE_STRIP = NO; 314 | DEBUG_INFORMATION_FORMAT = dwarf; 315 | ENABLE_STRICT_OBJC_MSGSEND = YES; 316 | ENABLE_TESTABILITY = YES; 317 | GCC_C_LANGUAGE_STANDARD = gnu11; 318 | GCC_DYNAMIC_NO_PIC = NO; 319 | GCC_NO_COMMON_BLOCKS = YES; 320 | GCC_OPTIMIZATION_LEVEL = 0; 321 | GCC_PREPROCESSOR_DEFINITIONS = ( 322 | "DEBUG=1", 323 | "$(inherited)", 324 | ); 325 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 326 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 327 | GCC_WARN_UNDECLARED_SELECTOR = YES; 328 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 329 | GCC_WARN_UNUSED_FUNCTION = YES; 330 | GCC_WARN_UNUSED_VARIABLE = YES; 331 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 332 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 333 | MTL_FAST_MATH = YES; 334 | ONLY_ACTIVE_ARCH = YES; 335 | OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=150 -Xfrontend -warn-long-function-bodies=150"; 336 | SDKROOT = iphoneos; 337 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 338 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 339 | }; 340 | name = Debug; 341 | }; 342 | 8363E0F322C4C59F00B55A7B /* Release */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | ALWAYS_SEARCH_USER_PATHS = NO; 346 | CLANG_ANALYZER_NONNULL = YES; 347 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 348 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 349 | CLANG_CXX_LIBRARY = "libc++"; 350 | CLANG_ENABLE_MODULES = YES; 351 | CLANG_ENABLE_OBJC_ARC = YES; 352 | CLANG_ENABLE_OBJC_WEAK = YES; 353 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 354 | CLANG_WARN_BOOL_CONVERSION = YES; 355 | CLANG_WARN_COMMA = YES; 356 | CLANG_WARN_CONSTANT_CONVERSION = YES; 357 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 358 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 359 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 360 | CLANG_WARN_EMPTY_BODY = YES; 361 | CLANG_WARN_ENUM_CONVERSION = YES; 362 | CLANG_WARN_INFINITE_RECURSION = YES; 363 | CLANG_WARN_INT_CONVERSION = YES; 364 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 365 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 366 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 367 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 368 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 369 | CLANG_WARN_STRICT_PROTOTYPES = YES; 370 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 371 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 372 | CLANG_WARN_UNREACHABLE_CODE = YES; 373 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 374 | COPY_PHASE_STRIP = NO; 375 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 376 | ENABLE_NS_ASSERTIONS = NO; 377 | ENABLE_STRICT_OBJC_MSGSEND = YES; 378 | GCC_C_LANGUAGE_STANDARD = gnu11; 379 | GCC_NO_COMMON_BLOCKS = YES; 380 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 381 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 382 | GCC_WARN_UNDECLARED_SELECTOR = YES; 383 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 384 | GCC_WARN_UNUSED_FUNCTION = YES; 385 | GCC_WARN_UNUSED_VARIABLE = YES; 386 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 387 | MTL_ENABLE_DEBUG_INFO = NO; 388 | MTL_FAST_MATH = YES; 389 | OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-expression-type-checking=150 -Xfrontend -warn-long-function-bodies=150"; 390 | SDKROOT = iphoneos; 391 | SWIFT_COMPILATION_MODE = wholemodule; 392 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 393 | VALIDATE_PRODUCT = YES; 394 | }; 395 | name = Release; 396 | }; 397 | 8363E0F522C4C59F00B55A7B /* Debug */ = { 398 | isa = XCBuildConfiguration; 399 | buildSettings = { 400 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 401 | CODE_SIGN_STYLE = Automatic; 402 | DEVELOPMENT_ASSET_PATHS = "SwiftTalk/Preview\\ Content"; 403 | ENABLE_PREVIEWS = YES; 404 | INFOPLIST_FILE = SwiftTalk/Info.plist; 405 | LD_RUNPATH_SEARCH_PATHS = ( 406 | "$(inherited)", 407 | "@executable_path/Frameworks", 408 | ); 409 | PRODUCT_BUNDLE_IDENTIFIER = io.objc.SwiftTalk; 410 | PRODUCT_NAME = "$(TARGET_NAME)"; 411 | SWIFT_VERSION = 5.0; 412 | TARGETED_DEVICE_FAMILY = "1,2"; 413 | }; 414 | name = Debug; 415 | }; 416 | 8363E0F622C4C59F00B55A7B /* Release */ = { 417 | isa = XCBuildConfiguration; 418 | buildSettings = { 419 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 420 | CODE_SIGN_STYLE = Automatic; 421 | DEVELOPMENT_ASSET_PATHS = "SwiftTalk/Preview\\ Content"; 422 | ENABLE_PREVIEWS = YES; 423 | INFOPLIST_FILE = SwiftTalk/Info.plist; 424 | LD_RUNPATH_SEARCH_PATHS = ( 425 | "$(inherited)", 426 | "@executable_path/Frameworks", 427 | ); 428 | PRODUCT_BUNDLE_IDENTIFIER = io.objc.SwiftTalk; 429 | PRODUCT_NAME = "$(TARGET_NAME)"; 430 | SWIFT_VERSION = 5.0; 431 | TARGETED_DEVICE_FAMILY = "1,2"; 432 | }; 433 | name = Release; 434 | }; 435 | /* End XCBuildConfiguration section */ 436 | 437 | /* Begin XCConfigurationList section */ 438 | 8363E0DB22C4C59E00B55A7B /* Build configuration list for PBXProject "SwiftTalk" */ = { 439 | isa = XCConfigurationList; 440 | buildConfigurations = ( 441 | 8363E0F222C4C59F00B55A7B /* Debug */, 442 | 8363E0F322C4C59F00B55A7B /* Release */, 443 | ); 444 | defaultConfigurationIsVisible = 0; 445 | defaultConfigurationName = Release; 446 | }; 447 | 8363E0F422C4C59F00B55A7B /* Build configuration list for PBXNativeTarget "SwiftTalk" */ = { 448 | isa = XCConfigurationList; 449 | buildConfigurations = ( 450 | 8363E0F522C4C59F00B55A7B /* Debug */, 451 | 8363E0F622C4C59F00B55A7B /* Release */, 452 | ); 453 | defaultConfigurationIsVisible = 0; 454 | defaultConfigurationName = Release; 455 | }; 456 | /* End XCConfigurationList section */ 457 | 458 | /* Begin XCRemoteSwiftPackageReference section */ 459 | 8325D5DA22E098380087B423 /* XCRemoteSwiftPackageReference "keychain-item" */ = { 460 | isa = XCRemoteSwiftPackageReference; 461 | repositoryURL = "https://github.com/objcio/keychain-item"; 462 | requirement = { 463 | kind = exactVersion; 464 | version = 0.1.0; 465 | }; 466 | }; 467 | 8363E0F722C4C7FE00B55A7B /* XCRemoteSwiftPackageReference "tiny-networking" */ = { 468 | isa = XCRemoteSwiftPackageReference; 469 | repositoryURL = "https://github.com/objcio/tiny-networking"; 470 | requirement = { 471 | branch = master; 472 | kind = branch; 473 | }; 474 | }; 475 | 8363E0FA22C4C83F00B55A7B /* XCRemoteSwiftPackageReference "swift-talk-shared" */ = { 476 | isa = XCRemoteSwiftPackageReference; 477 | repositoryURL = "https://github.com/objcio/swift-talk-shared"; 478 | requirement = { 479 | kind = upToNextMajorVersion; 480 | minimumVersion = 0.3.3; 481 | }; 482 | }; 483 | /* End XCRemoteSwiftPackageReference section */ 484 | 485 | /* Begin XCSwiftPackageProductDependency section */ 486 | 8325D5DB22E098380087B423 /* KeychainItem */ = { 487 | isa = XCSwiftPackageProductDependency; 488 | package = 8325D5DA22E098380087B423 /* XCRemoteSwiftPackageReference "keychain-item" */; 489 | productName = KeychainItem; 490 | }; 491 | 8363E0F822C4C7FE00B55A7B /* TinyNetworking */ = { 492 | isa = XCSwiftPackageProductDependency; 493 | package = 8363E0F722C4C7FE00B55A7B /* XCRemoteSwiftPackageReference "tiny-networking" */; 494 | productName = TinyNetworking; 495 | }; 496 | 8363E0FB22C4C83F00B55A7B /* Model */ = { 497 | isa = XCSwiftPackageProductDependency; 498 | package = 8363E0FA22C4C83F00B55A7B /* XCRemoteSwiftPackageReference "swift-talk-shared" */; 499 | productName = Model; 500 | }; 501 | 8363E0FD22C4C83F00B55A7B /* ViewHelpers */ = { 502 | isa = XCSwiftPackageProductDependency; 503 | package = 8363E0FA22C4C83F00B55A7B /* XCRemoteSwiftPackageReference "swift-talk-shared" */; 504 | productName = ViewHelpers; 505 | }; 506 | /* End XCSwiftPackageProductDependency section */ 507 | }; 508 | rootObject = 8363E0D822C4C59E00B55A7B /* Project object */; 509 | } 510 | -------------------------------------------------------------------------------- /SwiftTalk.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftTalk.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /SwiftTalk.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "KeychainItem", 6 | "repositoryURL": "https://github.com/objcio/keychain-item", 7 | "state": { 8 | "branch": null, 9 | "revision": "563fc973ee4c1fcdea646630f847a241025da3c4", 10 | "version": "0.1.0" 11 | } 12 | }, 13 | { 14 | "package": "swift-talk-shared", 15 | "repositoryURL": "https://github.com/objcio/swift-talk-shared", 16 | "state": { 17 | "branch": null, 18 | "revision": "0238161c1bf1a3db01c6d51b801e6d02656199b4", 19 | "version": "0.3.3" 20 | } 21 | }, 22 | { 23 | "package": "TinyNetworking", 24 | "repositoryURL": "https://github.com/objcio/tiny-networking", 25 | "state": { 26 | "branch": "master", 27 | "revision": "7e898a21962428316c36fd40f9949e60b90a0e65", 28 | "version": null 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /SwiftTalk.xcodeproj/project.xcworkspace/xcuserdata/chris.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objcio/swift-talk-app-swiftui/7412d2c123f9917b8f13b0e77b04d930c40de30d/SwiftTalk.xcodeproj/project.xcworkspace/xcuserdata/chris.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SwiftTalk.xcodeproj/project.xcworkspace/xcuserdata/florian.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/objcio/swift-talk-app-swiftui/7412d2c123f9917b8f13b0e77b04d930c40de30d/SwiftTalk.xcodeproj/project.xcworkspace/xcuserdata/florian.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /SwiftTalk.xcodeproj/xcuserdata/chris.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /SwiftTalk.xcodeproj/xcuserdata/chris.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftTalk.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SwiftTalk.xcodeproj/xcuserdata/florian.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftTalk.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /SwiftTalk/AllEpisodes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllEpisodes.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 04.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Model 11 | import TinyNetworking 12 | 13 | struct EpisodeItem: View { 14 | let episode: EpisodeView 15 | @ObservedObject var store = sharedStore 16 | 17 | var body: some View { 18 | HStack { 19 | VStack(alignment: .leading, spacing: 2) { 20 | Text(store.collection(for: episode)?.title ?? "") 21 | .font(.subheadline) 22 | .foregroundColor(.blue) 23 | Text(episode.title) 24 | .font(.headline) 25 | Text(episode.metaInfo) 26 | .font(.subheadline) 27 | .foregroundColor(.gray) 28 | } 29 | } 30 | } 31 | } 32 | 33 | struct AllEpisodes : View { 34 | let episodes: [EpisodeView] 35 | 36 | var body: some View { 37 | List { 38 | ForEach(episodes) { episode in 39 | NavigationLink(destination: Episode(episode: episode)) { 40 | EpisodeItem(episode: episode) 41 | } 42 | } 43 | }.navigationBarTitle("All Episodes") 44 | } 45 | } 46 | 47 | #if DEBUG 48 | struct AllEpisodes_Previews : PreviewProvider { 49 | static var previews: some View { 50 | AllEpisodes(episodes: sampleEpisodes) 51 | } 52 | } 53 | #endif 54 | -------------------------------------------------------------------------------- /SwiftTalk/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 27.06.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | func applicationWillTerminate(_ application: UIApplication) { 22 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 23 | } 24 | 25 | // MARK: UISceneSession Lifecycle 26 | 27 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 28 | // Called when a new scene session is being created. 29 | // Use this method to select a configuration to create the new scene with. 30 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 31 | } 32 | 33 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 34 | // Called when the user discards a scene session. 35 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 36 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 37 | } 38 | 39 | 40 | } 41 | 42 | -------------------------------------------------------------------------------- /SwiftTalk/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /SwiftTalk/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SwiftTalk/AuthenticationHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticationHelpers.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 18.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import UIKit 11 | import AuthenticationServices 12 | 13 | fileprivate var p: Provider? = nil 14 | fileprivate var authSession: ASWebAuthenticationSession? 15 | 16 | extension URLComponents { 17 | subscript(query name: String) -> String? { 18 | return queryItems?.first(where: { $0.name == name })?.value 19 | } 20 | } 21 | 22 | class Provider: NSObject, ASWebAuthenticationPresentationContextProviding { 23 | let window: UIWindow 24 | 25 | init(window: UIWindow) { 26 | self.window = window 27 | } 28 | func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { 29 | return window 30 | } 31 | } 32 | 33 | enum AuthenticationError: Error { 34 | case unknownError 35 | case authenticationError(Error) 36 | case parsingError 37 | } 38 | 39 | func getAuthToken(window: UIWindow, onComplete: @escaping (Result<(sessionId: String, csrf: String), AuthenticationError>) -> ()) { 40 | p = Provider(window: window) 41 | let callbackUrlScheme = "swifttalk://authorize" 42 | let authURL = URL(string: "https://talk.objc.io/users/auth/github?origin=/authorize_app")! 43 | 44 | //Initialize auth session 45 | authSession = ASWebAuthenticationSession(url: authURL, callbackURLScheme: callbackUrlScheme) { (callback: URL?, error: Error?) in 46 | if let e = error { 47 | onComplete(.failure(.authenticationError(e))) 48 | return 49 | } 50 | guard let successURL = callback else { 51 | onComplete(.failure(.unknownError)) 52 | return 53 | } 54 | 55 | guard let components = URLComponents(url: successURL, resolvingAgainstBaseURL: false), 56 | let id = components[query: "session_id"], let csrf = components[query: "csrf"] else { 57 | onComplete(.failure(.parsingError)) 58 | return 59 | } 60 | 61 | onComplete(.success((id, csrf))) 62 | } 63 | authSession?.presentationContextProvider = p 64 | authSession!.start() 65 | } 66 | -------------------------------------------------------------------------------- /SwiftTalk/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SwiftTalk/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Helpers.swift 3 | // SwiftTalk 4 | // 5 | // Created by Florian Kugler on 02.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import TinyNetworking 11 | 12 | struct ImageError: Error {} 13 | 14 | extension Endpoint where A == UIImage { 15 | init(imageURL url: URL) { 16 | self.init(.get, url: url, expectedStatusCode: expected200to300) { data, _ in 17 | guard let d = data, let i = UIImage(data: d) else { 18 | return .failure(ImageError()) 19 | } 20 | return .success(i) 21 | } 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /SwiftTalk/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleURLTypes 20 | 21 | 22 | CFBundleTypeRole 23 | Viewer 24 | CFBundleURLName 25 | io.objc.swifttalk 26 | CFBundleURLSchemes 27 | 28 | swifttalk 29 | 30 | 31 | 32 | CFBundleVersion 33 | 1 34 | LSRequiresIPhoneOS 35 | 36 | UIApplicationSceneManifest 37 | 38 | UIApplicationSupportsMultipleScenes 39 | 40 | UISceneConfigurations 41 | 42 | UIWindowSceneSessionRoleApplication 43 | 44 | 45 | UILaunchStoryboardName 46 | LaunchScreen 47 | UISceneConfigurationName 48 | Default Configuration 49 | UISceneDelegateClassName 50 | $(PRODUCT_MODULE_NAME).SceneDelegate 51 | 52 | 53 | 54 | 55 | UILaunchStoryboardName 56 | LaunchScreen 57 | UIRequiredDeviceCapabilities 58 | 59 | armv7 60 | 61 | UISupportedInterfaceOrientations 62 | 63 | UIInterfaceOrientationPortrait 64 | UIInterfaceOrientationLandscapeLeft 65 | UIInterfaceOrientationLandscapeRight 66 | 67 | UISupportedInterfaceOrientations~ipad 68 | 69 | UIInterfaceOrientationPortrait 70 | UIInterfaceOrientationPortraitUpsideDown 71 | UIInterfaceOrientationLandscapeLeft 72 | UIInterfaceOrientationLandscapeRight 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /SwiftTalk/Model/Account.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Account.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 18.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct WindowKey: EnvironmentKey { 12 | static let defaultValue: UIWindow? = nil 13 | } 14 | 15 | extension EnvironmentValues { 16 | var window: UIWindow? { 17 | get { 18 | self[WindowKey.self] 19 | } 20 | set { 21 | self[WindowKey.self] = newValue 22 | } 23 | } 24 | } 25 | 26 | struct Account: View { 27 | @Environment(\.window) var window 28 | @ObservedObject var session = Session.shared 29 | var body: some View { 30 | Form { 31 | if session.credentials == nil { 32 | Button(action: { 33 | getAuthToken(window: self.window!, onComplete: { result in 34 | switch result { 35 | case let .failure(e): print(e) // todo 36 | case let .success(info): 37 | self.session.credentials = info 38 | } 39 | }) 40 | }) { 41 | Text("Log In") 42 | } 43 | } else { 44 | Button(action: { self.session.credentials = nil }) { 45 | Text("Log Out") 46 | } 47 | } 48 | }.navigationBarTitle("Account") 49 | } 50 | } 51 | 52 | #if DEBUG 53 | struct Account_Previews: PreviewProvider { 54 | static var previews: some View { 55 | Account() 56 | } 57 | } 58 | #endif 59 | -------------------------------------------------------------------------------- /SwiftTalk/Model/EpisodeProgress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EpisodeProgress.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 29.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import Combine 12 | import Model 13 | 14 | final class EpisodeProgress: ObservableObject { 15 | let objectWillChange = PassthroughSubject() 16 | let sink: AnyCancellable 17 | let episode: EpisodeView 18 | var progress: TimeInterval { 19 | willSet { 20 | objectWillChange.send(newValue) 21 | } 22 | } 23 | init(episode: EpisodeView, progress: TimeInterval) { 24 | self.episode = episode 25 | self.progress = progress 26 | sink = objectWillChange 27 | .throttle(for: 10, scheduler: RunLoop.main, latest: true) 28 | .removeDuplicates() 29 | .sink { time in 30 | guard let resource = Session.shared.server.authenticated?.playProgress(episode: episode, progress: Int(time)) else { return } 31 | URLSession.shared.load(resource, onComplete: { print($0) }) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /SwiftTalk/Model/Model.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Model.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 27.06.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | import TinyNetworking 12 | import Model 13 | 14 | extension CollectionView: Identifiable {} 15 | extension EpisodeView: Identifiable {} 16 | 17 | extension EpisodeView { 18 | var metaInfo: String { 19 | "#\(number) · \(TimeInterval(media_duration).hoursAndMinutes) · \(released_at.pretty)" 20 | } 21 | 22 | var mediaURL: URL? { 23 | self.hls_url ?? self.preview_url 24 | } 25 | } 26 | 27 | extension CollectionView { 28 | var episodeCountAndTotalDuration: String { 29 | "\(episodes_count) episodes ᐧ \(TimeInterval(total_duration).hoursAndMinutes)" 30 | } 31 | } 32 | 33 | extension Session { 34 | var server: Server { 35 | let creds = credentials.map { 36 | Credentials(sessionId: $0.sessionId, csrf: $0.csrf) 37 | } 38 | return Server(credentials: creds) 39 | } 40 | } 41 | 42 | let sampleCollections: [CollectionView] = sample(name: "collections") 43 | let sampleEpisodes: [EpisodeView] = sample(name: "episodes") 44 | 45 | func sample(name: String) -> A { 46 | let url = Bundle.main.url(forResource: name, withExtension: "json")! 47 | let data = try! Data(contentsOf: url) 48 | return try! decoder.decode(A.self, from: data) 49 | } 50 | 51 | -------------------------------------------------------------------------------- /SwiftTalk/Model/Session.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Session.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 18.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import KeychainItem 11 | import SwiftUI 12 | import Combine 13 | 14 | final class Session: ObservableObject { 15 | @KeychainItem(account: "sessionId") private var sessionId 16 | @KeychainItem(account: "csrf") private var csrf 17 | 18 | var credentials: (sessionId: String, csrf: String)? { 19 | get { 20 | guard let s = sessionId, let c = csrf else { return nil } 21 | return (s, c) 22 | } 23 | set { 24 | objectWillChange.send() 25 | sessionId = newValue?.sessionId 26 | csrf = newValue?.csrf 27 | } 28 | } 29 | 30 | static let shared = Session() 31 | } 32 | -------------------------------------------------------------------------------- /SwiftTalk/Model/Store.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Store.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 29.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Model 11 | import SwiftUI 12 | import Combine 13 | 14 | final class Store: ObservableObject { 15 | let objectWillChange: AnyPublisher<(), Never> 16 | let sharedCollections = Resource(endpoint: Session.shared.server.allCollections) 17 | let sharedEpisodes = Resource(endpoint: Session.shared.server.allEpisodes) 18 | 19 | init() { 20 | objectWillChange = sharedCollections.objectWillChange.zip(sharedEpisodes.objectWillChange).map { _ in () }.eraseToAnyPublisher() 21 | } 22 | 23 | var loaded: Bool { 24 | sharedCollections.value != nil && sharedEpisodes.value != nil 25 | } 26 | 27 | var collections: [CollectionView] { sharedCollections.value ?? [] } 28 | var episodes: [EpisodeView] { sharedEpisodes.value ?? [] } 29 | 30 | func collection(for ep: EpisodeView) -> CollectionView? { 31 | collections.first { $0.id == ep.collection } 32 | } 33 | } 34 | 35 | let sharedStore = Store() 36 | -------------------------------------------------------------------------------- /SwiftTalk/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /SwiftTalk/Resource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Resource.swift 3 | // SwiftTalk2 4 | // 5 | // Created by Chris Eidhof on 27.06.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import TinyNetworking 11 | import Combine 12 | import SwiftUI 13 | 14 | final class Resource: ObservableObject { 15 | @Published var value: A? = nil 16 | // todo empty publisher 17 | var objectWillChange: AnyPublisher = Publishers.Sequence<[A?], Never>(sequence: []).eraseToAnyPublisher() 18 | let endpoint: Endpoint 19 | private var firstLoad = true 20 | 21 | init(endpoint: Endpoint) { 22 | self.endpoint = endpoint 23 | self.objectWillChange = $value.handleEvents(receiveSubscription: { [weak self] sub in 24 | guard let s = self, s.firstLoad else { return } 25 | s.firstLoad = false 26 | s.reload() 27 | 28 | }).eraseToAnyPublisher() 29 | } 30 | 31 | func reload() { 32 | URLSession.shared.load(endpoint) { result in 33 | DispatchQueue.main.async { 34 | self.value = try? result.get() 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /SwiftTalk/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 27.06.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 21 | 22 | // Use a UIHostingController as window root view controller 23 | if let windowScene = scene as? UIWindowScene { 24 | let window = UIWindow(windowScene: windowScene) 25 | let contentView = ContentView().environment(\.window, window) 26 | window.rootViewController = UIHostingController(rootView: contentView) 27 | self.window = window 28 | window.makeKeyAndVisible() 29 | } 30 | } 31 | 32 | func sceneDidDisconnect(_ scene: UIScene) { 33 | // Called as the scene is being released by the system. 34 | // This occurs shortly after the scene enters the background, or when its session is discarded. 35 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 36 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 37 | } 38 | 39 | func sceneDidBecomeActive(_ scene: UIScene) { 40 | // Called when the scene has moved from an inactive state to an active state. 41 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 42 | } 43 | 44 | func sceneWillResignActive(_ scene: UIScene) { 45 | // Called when the scene will move from an active state to an inactive state. 46 | // This may occur due to temporary interruptions (ex. an incoming phone call). 47 | } 48 | 49 | func sceneWillEnterForeground(_ scene: UIScene) { 50 | // Called as the scene transitions from the background to the foreground. 51 | // Use this method to undo the changes made on entering the background. 52 | } 53 | 54 | func sceneDidEnterBackground(_ scene: UIScene) { 55 | // Called as the scene transitions from the foreground to the background. 56 | // Use this method to save data, release shared resources, and store enough scene-specific state information 57 | // to restore the scene back to its current state. 58 | } 59 | 60 | 61 | } 62 | 63 | -------------------------------------------------------------------------------- /SwiftTalk/ViewHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewHelpers.swift 3 | // SwiftTalk 4 | // 5 | // Created by Florian Kugler on 02.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct NewBadge: View { 12 | var body: some View { 13 | Text("NEW").foregroundColor(.white).font(.footnote).padding(5).background(Color.blue.cornerRadius(5)) 14 | } 15 | } 16 | 17 | struct LazyView: View { 18 | let build: () -> V 19 | 20 | init(_ build: @escaping @autoclosure () -> V) { 21 | self.build = build 22 | } 23 | 24 | var body: V { 25 | build() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /SwiftTalk/Views/AllEpisodes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AllEpisodes.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 04.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct AllEpisodes : View { 12 | var body: some View { 13 | Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/) 14 | } 15 | } 16 | 17 | #if DEBUG 18 | struct AllEpisodes_Previews : PreviewProvider { 19 | static var previews: some View { 20 | AllEpisodes() 21 | } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /SwiftTalk/Views/CollectionDetails.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionDetails.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 27.06.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import TinyNetworking 11 | import Model 12 | 13 | struct CollectionDetails : View { 14 | let collection: CollectionView 15 | @ObservedObject var store = sharedStore 16 | @ObservedObject var image: Resource 17 | var collectionEpisodes: [EpisodeView] { 18 | return store.episodes.filter { $0.collection == collection.id } 19 | } 20 | init(collection: CollectionView) { 21 | self.collection = collection 22 | self.image = Resource(endpoint: Endpoint(imageURL: collection.artwork.png)) 23 | } 24 | var body: some View { 25 | List { 26 | VStack(alignment: .leading) { 27 | if image.value != nil { 28 | Image(uiImage: image.value!) 29 | .resizable() 30 | .aspectRatio(16/9, contentMode: .fit) 31 | } else { 32 | Loader().aspectRatio(16/9, contentMode: .fit) 33 | } 34 | VStack(alignment: .leading) { 35 | HStack { 36 | Text(collection.title) 37 | .font(.title) 38 | .fontWeight(.bold) 39 | .lineLimit(nil) 40 | if collection.new { 41 | NewBadge() 42 | } 43 | } 44 | Text(collection.episodeCountAndTotalDuration) 45 | .foregroundColor(.gray) 46 | .padding([.bottom]) 47 | Text(collection.description) 48 | .lineLimit(nil) 49 | } 50 | } 51 | ForEach(collectionEpisodes) { episode in 52 | NavigationLink(destination: Episode(episode: episode)) { 53 | VStack(alignment: .leading, spacing: 2) { 54 | Text(episode.title) 55 | .font(.headline) 56 | Text(episode.metaInfo) 57 | .font(.subheadline) 58 | .foregroundColor(.gray) 59 | } 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | #if DEBUG 67 | struct CollectionDetails_Previews : PreviewProvider { 68 | static var previews: some View { 69 | CollectionDetails(collection: sampleCollections[1]) 70 | } 71 | } 72 | #endif 73 | -------------------------------------------------------------------------------- /SwiftTalk/Views/CollectionsList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionsList.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 27.06.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Model 11 | import ViewHelpers 12 | 13 | struct CollectionsList : View { 14 | let collections: [CollectionView] 15 | var body: some View { 16 | List { 17 | ForEach(collections) { coll in 18 | NavigationLink(destination: CollectionDetails(collection: coll)) { 19 | HStack { 20 | VStack(alignment: .leading) { 21 | Text(coll.title) 22 | Text(coll.episodeCountAndTotalDuration) 23 | .font(.caption) 24 | .foregroundColor(.gray) 25 | } 26 | if coll.new { 27 | Spacer() 28 | NewBadge() 29 | } 30 | } 31 | } 32 | } 33 | }.navigationBarTitle(Text("Collections")) 34 | } 35 | } 36 | 37 | #if DEBUG 38 | struct CollectionsList_Previews : PreviewProvider { 39 | static var previews: some View { 40 | NavigationView { 41 | CollectionsList(collections: sampleCollections) 42 | } 43 | } 44 | } 45 | #endif 46 | -------------------------------------------------------------------------------- /SwiftTalk/Views/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 27.06.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import TinyNetworking 11 | import Model 12 | import ViewHelpers 13 | 14 | struct ContentView : View { 15 | @ObservedObject var store = sharedStore 16 | var body: some View { 17 | Group { 18 | if !store.loaded { 19 | Loader() 20 | .aspectRatio(16/9, contentMode: .fit) 21 | } else { 22 | TabView { 23 | NavigationView { 24 | CollectionsList(collections: store.collections) 25 | }.tabItem { Text("Collections" )}.tag(0) 26 | NavigationView { 27 | AllEpisodes(episodes: store.episodes) 28 | }.tabItem { Text("All Episodes" )}.tag(1) 29 | NavigationView { 30 | Account() 31 | }.tabItem { Text("Account") }.tag(2) 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | #if DEBUG 39 | struct ContentView_Previews : PreviewProvider { 40 | static var previews: some View { 41 | ContentView() 42 | } 43 | } 44 | #endif 45 | -------------------------------------------------------------------------------- /SwiftTalk/Views/Episode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Episode.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 04.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import Model 11 | import TinyNetworking 12 | 13 | struct PlayState { 14 | var isPlaying = false { 15 | didSet { 16 | if isPlaying { startedPlaying = true } 17 | } 18 | } 19 | var startedPlaying = false 20 | } 21 | 22 | struct Episode : View { 23 | let episode: EpisodeView 24 | @State var playState = PlayState() 25 | @ObservedObject var image: Resource 26 | @ObservedObject var progress: EpisodeProgress 27 | @ObservedObject var details: Resource 28 | init(episode: EpisodeView) { 29 | self.episode = episode 30 | self.image = Resource(endpoint: Endpoint(imageURL: episode.poster_url)) 31 | self.progress = EpisodeProgress(episode: episode, progress: 0) // todo real progress 32 | self.details = Resource(endpoint: Session.shared.server.episodeDetails(episode: episode)) 33 | } 34 | 35 | var overlay: AnyView? { 36 | if !playState.startedPlaying { 37 | return AnyView(Group { 38 | if image.value != nil { 39 | Image(uiImage: image.value!).resizable() 40 | } else { 41 | Loader() 42 | } 43 | }.aspectRatio(16/9, contentMode: .fit)) 44 | } else { 45 | return nil 46 | } 47 | } 48 | var body: some View { 49 | VStack(alignment: .leading, spacing: 12) { 50 | VStack (alignment: .leading) { 51 | Text(episode.title) 52 | .font(.largeTitle) 53 | .fontWeight(.bold) 54 | .lineLimit(nil) 55 | Text(episode.metaInfo) 56 | .foregroundColor(.gray) 57 | } 58 | Text(episode.synopsis) 59 | .lineLimit(nil) 60 | .padding([.bottom]) 61 | Player(url: episode.mediaURL!, isPlaying: $playState.isPlaying, playPosition: $progress.progress, overlay: overlay) 62 | .aspectRatio(16/9, contentMode: .fit) 63 | Slider(value: $progress.progress, in: 0...TimeInterval(episode.media_duration)) 64 | Spacer() 65 | }.padding([.leading, .trailing]) 66 | } 67 | } 68 | 69 | #if DEBUG 70 | struct Episode_Previews : PreviewProvider { 71 | static var previews: some View { 72 | Episode(episode: sampleEpisodes[0]) 73 | } 74 | } 75 | #endif 76 | -------------------------------------------------------------------------------- /SwiftTalk/Views/EpisodeDetail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EpisodeDetail.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 04.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct EpisodeDetail : View { 12 | var body: some View { 13 | Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/) 14 | } 15 | } 16 | 17 | #if DEBUG 18 | struct EpisodeDetail_Previews : PreviewProvider { 19 | static var previews: some View { 20 | EpisodeDetail() 21 | } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /SwiftTalk/Views/Loader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Loader.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 05.08.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SwiftUI 11 | 12 | struct Eight: Shape { 13 | func path(in rect: CGRect) -> Path { 14 | return Path { p in 15 | let start = CGPoint(x: 0.75, y: 0) 16 | p.move(to: start) 17 | p.addQuadCurve(to: CGPoint(x: 1, y: 0.5), control: CGPoint(x: 1, y: 0)) 18 | p.addQuadCurve(to: CGPoint(x: 0.75, y: 1), control: CGPoint(x: 1, y: 1)) 19 | p.addCurve(to: CGPoint(x: 0.25, y: 0), control1: CGPoint(x: 0.5, y: 1), control2: CGPoint(x: 0.5, y: 0)) 20 | p.addQuadCurve(to: CGPoint(x: 0, y: 0.5), control: CGPoint(x: 0, y: 0)) 21 | p.addQuadCurve(to: CGPoint(x: 0.25, y: 1), control: CGPoint(x: 0, y: 1)) 22 | p.addCurve(to: start, control1: CGPoint(x: 0.5, y: 1), control2: CGPoint(x: 0.5, y: 0)) 23 | }.applying(CGAffineTransform(scaleX: rect.width, y: rect.height)) 24 | } 25 | } 26 | 27 | extension Path { 28 | private func point(at position: CGFloat) -> CGPoint { 29 | var pos = position.remainder(dividingBy: 1) 30 | if pos < 0 { 31 | pos = 1 + pos 32 | } 33 | return pos > 0 ? trimmedPath(from: 0, to: position).cgPath.currentPoint : cgPath.currentPoint 34 | } 35 | 36 | func pointAndAngle(at position: CGFloat) -> (CGPoint, Angle) { 37 | let epsilon: CGFloat = 1e-3 38 | let p1 = point(at: position - epsilon) 39 | let p2 = point(at: position) 40 | let atan = atan2(p2.y - p1.y, p2.x - p1.x) 41 | return (p2, Angle(radians: Double(atan))) 42 | } 43 | } 44 | 45 | let arrowStrokeStyle = StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round, miterLimit: 0, dash: [], dashPhase: 0) 46 | 47 | struct FollowPath: GeometryEffect { 48 | var position: CGFloat // 0...1 49 | var shape: S 50 | 51 | var animatableData: AnimatablePair { 52 | get { 53 | AnimatablePair(position, shape.animatableData) 54 | } 55 | set { 56 | self.position = newValue.first 57 | shape.animatableData = newValue.second 58 | } 59 | } 60 | 61 | func effectValue(size: CGSize) -> ProjectionTransform { 62 | let rect = CGRect(origin: .zero, size: size) 63 | let path = shape.path(in: rect) 64 | let (point, angle) = path.pointAndAngle(at: position) 65 | let affineTransform = CGAffineTransform(translationX: point.x, y: point.y).rotated(by: CGFloat(angle.radians + Double.pi/2)) 66 | return ProjectionTransform(affineTransform) 67 | } 68 | } 69 | 70 | struct Trail: Shape { 71 | let _path: P 72 | var position: CGFloat 73 | var trailLength: CGFloat 74 | 75 | init(path: P, at position: CGFloat, trailLength: CGFloat) { 76 | self._path = path 77 | self.position = position 78 | self.trailLength = trailLength 79 | } 80 | 81 | var animatableData: AnimatablePair { 82 | get { 83 | AnimatablePair(position, trailLength) 84 | } 85 | set { 86 | position = newValue.first 87 | trailLength = newValue.second 88 | } 89 | } 90 | 91 | func path(in rect: CGRect) -> Path { 92 | let path = _path.path(in: rect) 93 | let trimFrom = position - trailLength 94 | var result = Path() 95 | if trimFrom < 0 { 96 | result.addPath(path.trimmedPath(from: trimFrom + 1, to: 1)) 97 | } 98 | result.addPath(path.trimmedPath(from: max(0, trimFrom), to: position)) 99 | return result 100 | } 101 | } 102 | 103 | struct ArrowHead: Shape { 104 | func path(in rect: CGRect) -> Path { 105 | Path { p in 106 | p.move(to: CGPoint(x: 0, y: rect.size.width)) 107 | p.addLine(to: CGPoint(x: rect.size.width/2, y: 0)) 108 | p.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height)) 109 | }.offsetBy(dx: rect.origin.x, dy: rect.origin.y) 110 | .offsetBy(dx: 2, dy: 5) 111 | .strokedPath(arrowStrokeStyle) 112 | } 113 | } 114 | 115 | struct Loader: View { 116 | @State var trailLength: CGFloat = 0.15 117 | @State var position: CGFloat = 0 118 | var body: some View { 119 | ZStack { 120 | Trail(path: Eight(), at: position, trailLength: trailLength) 121 | .stroke(Color.black, style: arrowStrokeStyle) 122 | ArrowHead().size(width: 16, height: 16) 123 | .offset(x: -8, y: -8) 124 | .modifier(FollowPath(position: position, shape: Eight())) 125 | 126 | 127 | } 128 | .padding(20) 129 | .onAppear { 130 | withAnimation(Animation.linear(duration: 2).repeatForever(autoreverses: false)) { 131 | self.position = 1 132 | self.trailLength *= 2 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /SwiftTalk/Views/Player.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Player.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 04.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | import AVKit 11 | 12 | final class PlayerCoordinator { 13 | var observer: NSKeyValueObservation? 14 | var timeObserver: Any? 15 | var hostingViewController: UIHostingController? 16 | var lastObservedPosition = TimeInterval(0) 17 | } 18 | 19 | struct Player: UIViewControllerRepresentable { 20 | let url: URL 21 | @Binding var isPlaying: Bool 22 | @Binding var playPosition: TimeInterval 23 | let overlay: Overlay? 24 | 25 | func makeCoordinator() -> PlayerCoordinator { 26 | return PlayerCoordinator() 27 | } 28 | 29 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> AVPlayerViewController { 30 | let player = AVPlayer(url: url) 31 | 32 | context.coordinator.observer = player.observe(\.rate, options: [.new]) { _, change in 33 | self.isPlaying = (change.newValue ?? 0) > 0 34 | } 35 | 36 | context.coordinator.timeObserver = player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 1, preferredTimescale: 1), queue: .main) { time in 37 | let pos = time.seconds 38 | self.playPosition = pos 39 | context.coordinator.lastObservedPosition = pos 40 | } 41 | 42 | let playerVC = AVPlayerViewController() 43 | playerVC.player = player 44 | 45 | let hostingVC = UIHostingController(rootView: overlay) 46 | playerVC.addChild(hostingVC) 47 | playerVC.contentOverlayView?.addSubview(hostingVC.view) 48 | hostingVC.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 49 | hostingVC.didMove(toParent: playerVC) 50 | context.coordinator.hostingViewController = hostingVC 51 | 52 | return playerVC 53 | } 54 | 55 | func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext) { 56 | if context.coordinator.lastObservedPosition != playPosition { 57 | let time = CMTime(seconds: playPosition, preferredTimescale: 1) 58 | uiViewController.player?.seek(to: time) 59 | } 60 | guard let hc = context.coordinator.hostingViewController else { return } 61 | if let p = overlay { 62 | hc.rootView = p 63 | hc.view.isHidden = false 64 | } else { 65 | hc.view.isHidden = true 66 | } 67 | } 68 | 69 | static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: PlayerCoordinator) { 70 | if let o = coordinator.timeObserver { 71 | uiViewController.player?.removeTimeObserver(o) 72 | } 73 | } 74 | } 75 | 76 | #if DEBUG 77 | struct Player_Previews : PreviewProvider { 78 | static var previews: some View { 79 | Player(url: URL(string: "https://devstreaming-cdn.apple.com/videos/streaming/examples/img_bipbop_adv_example_fmp4/master.m3u8")!, isPlaying: .constant(false), playPosition: .constant(0), overlay: Text("sample overlay")) 80 | } 81 | } 82 | #endif 83 | -------------------------------------------------------------------------------- /SwiftTalk/Views/PlayerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerView.swift 3 | // SwiftTalk 4 | // 5 | // Created by Chris Eidhof on 04.07.19. 6 | // Copyright © 2019 Chris Eidhof. All rights reserved. 7 | // 8 | 9 | import SwiftUI 10 | 11 | struct PlayerView : View { 12 | var body: some View { 13 | Text(/*@START_MENU_TOKEN@*/"Hello World!"/*@END_MENU_TOKEN@*/) 14 | } 15 | } 16 | 17 | #if DEBUG 18 | struct PlayerView_Previews : PreviewProvider { 19 | static var previews: some View { 20 | PlayerView() 21 | } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /SwiftTalk/collections.json: -------------------------------------------------------------------------------- 1 | [{"total_duration":1970,"description":"We explore Apple's new declarative UI framework.","id":"swiftui","episodes_count":2,"title":"SwiftUI","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/SwiftUI@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/SwiftUI.svg"},"new":true,"url":"https:\/\/talk.objc.io\/collections\/swiftui"},{"total_duration":10490,"description":"We build a markdown editor for the Mac that can execute Swift code blocks. The app uses CommonMark to parse the markdown, and SwiftSyntax for syntax highlighting of Swift code blocks. For the execution of Swift code, we use a Swift REPL process.","id":"markdown-playgrounds","episodes_count":7,"title":"Markdown Playgrounds","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Markdown%20Playgrounds@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Markdown%20Playgrounds.svg"},"new":true,"url":"https:\/\/talk.objc.io\/collections\/markdown-playgrounds"},{"total_duration":10443,"description":"We build a routing feature for a running trail app — from visualizing the tracks on a map to building a graph and implementing the shortest path algorithm.","id":"map-routing","episodes_count":7,"title":"Map Routing","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Map%20Routing@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Map%20Routing.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/map-routing"},{"total_duration":10821,"description":"We build a layout library that makes it easy to support a wide range of screen sizes and dynamic type. Similar to responsive design on the web, our library allows to specify different layout variants and automatically chooses the one that fits best.","id":"building-a-layout-library","episodes_count":8,"title":"Building a Layout Library","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Building%20a%20Layout%20Library@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Building%20a%20Layout%20Library.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/building-a-layout-library"},{"total_duration":14556,"description":"We build a reusable, declarative form library by refactoring the specific code of a settings screen. The library automatically keeps the data backing the form in sync with the form on screen.","id":"building-a-form-library","episodes_count":10,"title":"Building a Form Library","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Building%20a%20Form%20Library@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Building%20a%20Form%20Library.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/building-a-form-library"},{"total_duration":9399,"description":"We explore a variety of techniques to refactor very large view controllers. As an example, we look at the largest view controller from the open source Wikipedia for iOS app.","id":"refactoring-large-view-controllers","episodes_count":6,"title":"Refactoring Large View Controllers","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Refactoring%20Large%20View%20Controllers@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Refactoring%20Large%20View%20Controllers.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/refactoring-large-view-controllers"},{"total_duration":23773,"description":"Swift is a complex language. While much of the more advanced features and oddities stay out of sight in everyday iOS development, studying these aspects provides many learning opportunities. This collection covers some of the lesser known aspects of the Swift languague.","id":"swift-the-language","episodes_count":20,"title":"Swift, the Language","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Swift,%20the%20Language@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Swift,%20the%20Language.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/swift-the-language"},{"total_duration":11912,"description":"Networking is part of almost every app, often with the help of large third-party libraries. In this collection we show how to build a custom lightweight and type-safe networking abstractions from scratch.","id":"networking","episodes_count":9,"title":"Networking","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Networking@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Networking.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/networking"},{"total_duration":26715,"description":"App architecture is a hot topic with new patterns emerging constantly. In this collection there are tips and experiments related to architectural decisions, ranging from small refactorings to global architecture patterns.","id":"architecture","episodes_count":20,"title":"Architecture","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Architecture@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Architecture.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/architecture"},{"total_duration":24939,"description":"With Swift we've seen a resurgence of functional programming patterns in the iOS community. This collection shows examples of functional programming.","id":"functional-programming","episodes_count":18,"title":"Functional Programming","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Functional%20Programming@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Functional%20Programming.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/functional-programming"},{"total_duration":5343,"description":"Since table views are a foundational building block of iOS apps, improving the code around them really pays off. For example, we show how table view code can be made more generic and thus more reusable, and how to drive table view animations.","id":"table-views","episodes_count":4,"title":"Table Views","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Table%20Views@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Table%20Views.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/table-views"},{"total_duration":17860,"description":"This collection mainly focuses on understanding the machinery behind reactive programming libraries. We start out with a quick look at how reactive code compares to imperative UIKit code. Then we build a simple reactive library.","id":"reactive-programming","episodes_count":12,"title":"Reactive Programming","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Reactive%20Programming@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Reactive%20Programming.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/reactive-programming"},{"total_duration":4739,"description":"Incremental programming is a technique similar to reactive programming. However, the propagation of values through the reactive graph is done in such a way that you don't have to worry about duplicate observer calls and reactive glitches.","id":"incremental-programming","episodes_count":3,"title":"Incremental Programming","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Incremental%20Programming@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Incremental%20Programming.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/incremental-programming"},{"total_duration":19233,"description":"This collection takes a framework-independent look at how to use Swift on the server. We use the Swift package manager and Docker to set up our development environment, add a simple HTTP server and build routing infrastructure on top. Lastly, we interface with PostgreSQL's C library for database access.","id":"server-side-swift","episodes_count":14,"title":"Server-Side Swift","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Server-Side%20Swift@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Server-Side%20Swift.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/server-side-swift"},{"total_duration":8020,"description":"Often we can make Cocoa APIs feel more at home in Swift by writing a tiny type-safe wrapper around them. In this collection we show examples of this pattern that clean up code throughout your project.","id":"type-safe-api-wrappers","episodes_count":6,"title":"Type-Safe API Wrappers","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Type-Safe%20API%20Wrappers@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Type-Safe%20API%20Wrappers.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/type-safe-api-wrappers"},{"total_duration":6922,"description":"We explore techniques for writing library code as well as factor out reusable components from specific code we've written before.","id":"libraries","episodes_count":5,"title":"Libraries","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Libraries@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Libraries.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/libraries"},{"total_duration":8022,"description":"Xcode 9 gave us a new code editor, Swift refactoring capabilities, and many other improvements. We demonstrate some of these features that have helped us to be more productive in Xcode.","id":"tooling","episodes_count":6,"title":"Tooling","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Tooling@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Tooling.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/tooling"},{"total_duration":7281,"description":"In this collection we're joined by Brandon and Lisa from Kickstarter. Kickstarter has a unique code base that is consistently architected around functional and reactive programming concepts, combined with an emphasis on testing. We explore some of these aspects in four episodes recorded in spring 2017.","id":"ios-at-kickstarter","episodes_count":4,"title":"iOS at Kickstarter","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/iOS%20at%20Kickstarter@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/iOS%20at%20Kickstarter.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/ios-at-kickstarter"},{"total_duration":5926,"description":"While Swift's collection protocols can be overwhelming at first, they provide a powerful infrastructure to extend collections' functionality and to implement entirely custom collection types. In this series we look at both aspects and learn about the underlying machinery in the process.","id":"collection-protocols","episodes_count":6,"title":"Collection Protocols","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Collection%20Protocols@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Collection%20Protocols.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/collection-protocols"},{"total_duration":6820,"description":"Swift's standard library provides many basic data strutures, such as arrays, dictionaries, and sets. In this collection we explore both built-in and custom data structures.","id":"data-structures","episodes_count":4,"title":"Data Structures","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Data%20Structures@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Data%20Structures.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/data-structures"},{"total_duration":12126,"description":"From time to time we explore expiremental ideas. Some of these might turn out to be really useful, others might stay in the realm of interesting exercises that teach us to better understand Swift.","id":"experiments","episodes_count":7,"title":"Experiments","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Experiments@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Experiments.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/experiments"},{"total_duration":11345,"description":"Ledger is a popular command line accounting system that we use for all our bookkeeping. While we love the text-based nature of it, we wanted to build a simple GUI around it to get better visibility into our data. Next to the basic AppKit shell, this project uses lots of functional parsing and evaluation code.","id":"ledger-mac-app","episodes_count":9,"title":"Ledger Mac App","artwork":{"png":"https:\/\/talk.objc.io\/assets\/images\/collections\/Ledger%20Mac%20App@2x.png","svg":"https:\/\/talk.objc.io\/assets\/images\/collections\/Ledger%20Mac%20App.svg"},"new":false,"url":"https:\/\/talk.objc.io\/collections\/ledger-mac-app"}] -------------------------------------------------------------------------------- /SwiftTalk/episodes.json: -------------------------------------------------------------------------------- 1 | [{"poster_url":"https:\/\/i.vimeocdn.com\/video\/791960035.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/343661035.m3u8?s=d92e89c4adfde5ac3eec4d219c6d116725e28117&oauth2_token_id=1138343922","released_at":1561125600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/791960035_590x270.jpg","collection":"swiftui","media_duration":918,"title":"Asynchronous Networking with SwiftUI","subscription_only":true,"number":157,"url":"https:\/\/talk.objc.io\/episodes\/S01E157-asynchronous-networking-with-swiftui","synopsis":"We integrate the tiny networking library into a SwiftUI project and wrap AppKit's progress indicator in a SwiftUI view."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/790988822.jpg","released_at":1560520800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/790988822_590x270.jpg","collection":"swiftui","hls_url":"https:\/\/player.vimeo.com\/external\/341987451.m3u8?s=a089ed898e78b446f044c74ece3552ea8401c4a1&oauth2_token_id=1138343922","media_duration":1052,"title":"A First Look at SwiftUI","subscription_only":false,"number":156,"url":"https:\/\/talk.objc.io\/episodes\/S01E156-a-first-look-at-swiftui","synopsis":"We build a simple currency converter to experiment with SwiftUI's state-driven view updates."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/788679769.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/340932507.m3u8?s=dc42751648f84c2541ff4e5a73428b4cfeb20326&oauth2_token_id=1138343922","released_at":1559916000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/788679769_590x270.jpg","collection":"markdown-playgrounds","media_duration":1275,"title":"Improving Performance","subscription_only":true,"number":155,"url":"https:\/\/talk.objc.io\/episodes\/S01E155-improving-performance","synopsis":"We improve the performance of syntax highlighting to be able to work in large documents."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/786393240.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/339498315.m3u8?s=adad7f915ca4b8949afaf1a005e1e3e075a91a52&oauth2_token_id=1138343922","released_at":1559311200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/786393240_590x270.jpg","collection":"markdown-playgrounds","media_duration":1157,"title":"Building a Link Checker","subscription_only":true,"number":154,"url":"https:\/\/talk.objc.io\/episodes\/S01E154-building-a-link-checker","synopsis":"We use several pieces of code from earlier episodes to build a link checking extension on URLSession."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/780080067.jpg","released_at":1558706400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/780080067_590x270.jpg","collection":"functional-programming","hls_url":"https:\/\/player.vimeo.com\/external\/333936786.m3u8?s=c96d1f7ec6d294647ce3f2fb70db16e66b4c239f&oauth2_token_id=1138343922","media_duration":1164,"title":"Making Impossible States Impossible","subscription_only":false,"number":153,"url":"https:\/\/talk.objc.io\/episodes\/S01E153-making-impossible-states-impossible","synopsis":"We discuss how Swift's type system can be used to eliminate impossible states from our code."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/779225094.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/336818209.m3u8?s=56e3a45d082a39087ad76cf3037ef88266045227&oauth2_token_id=1138343922","released_at":1558101600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/779225094_590x270.jpg","collection":"functional-programming","media_duration":1659,"title":"Processing CommonMark using Folds","subscription_only":true,"number":152,"url":"https:\/\/talk.objc.io\/episodes\/S01E152-processing-commonmark-using-folds","synopsis":"We implement fold on the CommonMark syntax tree and use it to extract links and text from a document."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/779206263.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/335383997.m3u8?s=6a44cd71a5f609d929badc80f089db8ab3504354&oauth2_token_id=1138343922","released_at":1557496800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/779206263_590x270.jpg","collection":"functional-programming","media_duration":1406,"title":"Reduce vs. Fold","subscription_only":true,"number":151,"url":"https:\/\/talk.objc.io\/episodes\/S01E151-reduce-vs-fold","synopsis":"We explore the differences between reduce and fold and how they can be implemented on any enum."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/778914330.jpg","released_at":1556892000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/778914330_590x270.jpg","collection":"functional-programming","hls_url":"https:\/\/player.vimeo.com\/external\/333042859.m3u8?s=5a75ada700760696deef9527f78154f5c610aebc&oauth2_token_id=1138343922","media_duration":1417,"title":"The Origins of Reduce","subscription_only":false,"number":150,"url":"https:\/\/talk.objc.io\/episodes\/S01E150-the-origins-of-reduce","synopsis":"Wouter joins us to explore the origins of the reduce function."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/776184830.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/332672172.m3u8?s=a5cf8747a10d06de8b367558c2b69544ac6568e8&oauth2_token_id=1138343922","released_at":1556287200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/776184830_590x270.jpg","collection":"markdown-playgrounds","media_duration":1781,"title":"Swift Syntax Highlighting","subscription_only":true,"number":149,"url":"https:\/\/talk.objc.io\/episodes\/S01E149-swift-syntax-highlighting","synopsis":"We use SwiftSyntax to add highlighting for Swift code blocks."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/773772314.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/331419994.m3u8?s=ddbe2f578197d50d812de352015d1e522a22ef61&oauth2_token_id=1138343922","released_at":1555682400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/773772314_590x270.jpg","collection":"markdown-playgrounds","media_duration":1787,"title":"String Handling","subscription_only":true,"number":148,"url":"https:\/\/talk.objc.io\/episodes\/S01E148-string-handling","synopsis":"We fix a couple of String-related bugs when interoperating with the cmark library and the REPL."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/772881880.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/330028216.m3u8?s=afba06e3831d9295280cb5c10dfc79910651f536&oauth2_token_id=1138343922","released_at":1555077600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/772881880_590x270.jpg","collection":"markdown-playgrounds","media_duration":1391,"title":"Executing Swift Code","subscription_only":true,"number":147,"url":"https:\/\/talk.objc.io\/episodes\/S01E147-executing-swift-code","synopsis":"We launch a Swift REPL process to execute Swift code in our Markdown file."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/769420413.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/328628729.m3u8?s=705b1cfcb9d1ca808a0f58c90bf090f09afeecda&oauth2_token_id=1138343922","released_at":1554476400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/769420413_590x270.jpg","collection":"markdown-playgrounds","media_duration":1587,"title":"Markdown Syntax Highlighting","subscription_only":true,"number":146,"url":"https:\/\/talk.objc.io\/episodes\/S01E146-markdown-syntax-highlighting","synopsis":"We use CommonMark to parse the markdown string and then add attributes to highlight its syntax."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/769411132.jpg","released_at":1553871600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/769411132_590x270.jpg","collection":"markdown-playgrounds","hls_url":"https:\/\/player.vimeo.com\/external\/325616799.m3u8?s=2dd562c101df523ce70314eb4cbf11c6615c2327&oauth2_token_id=1138343922","media_duration":1512,"title":"Setting Up a Document-Based App","subscription_only":false,"number":145,"url":"https:\/\/talk.objc.io\/episodes\/S01E145-setting-up-a-document-based-app","synopsis":"We start building our Markdown Playgrounds app from a plain command-line app package, leveraging AppKit's document architecture."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/762373000.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/325845017.m3u8?s=2052f3fdd9d00c0ebfed7820189b773c40bb7fa4&oauth2_token_id=1138343922","released_at":1553266800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/762373000_590x270.jpg","collection":"swift-the-language","media_duration":897,"title":"String Interpolation in Swift 5 (Part 2)","subscription_only":true,"number":144,"url":"https:\/\/talk.objc.io\/episodes\/S01E144-string-interpolation-in-swift-5-part-2","synopsis":"We refactor our string interpolation code to allow concatenation of multiple interpolated SQL queries."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/762357484.jpg","released_at":1552662000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/762357484_590x270.jpg","collection":"swift-the-language","hls_url":"https:\/\/player.vimeo.com\/external\/319897843.m3u8?s=e3e511aaf0b3b1e9e4c76ab17bc8cc86b0a72478&oauth2_token_id=1138343922","media_duration":1007,"title":"String Interpolation in Swift 5","subscription_only":false,"number":143,"url":"https:\/\/talk.objc.io\/episodes\/S01E143-string-interpolation-in-swift-5","synopsis":"We use Swift 5's new string interpolation API to automatically insert placeholders in SQL queries."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/761471025.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/322232795.m3u8?s=3e45c13d82f090d691fefa3622f4c36747a8025b&oauth2_token_id=1138343922","released_at":1552057200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/761471025_590x270.jpg","collection":"server-side-swift","media_duration":1091,"title":"Flow Testing with Protocols","subscription_only":true,"number":142,"url":"https:\/\/talk.objc.io\/episodes\/S01E142-flow-testing-with-protocols","synopsis":"Using a protocol-based approach, we show how we write tests for the Swift Talk backend."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/761471249.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/320739448.m3u8?s=0d0a52e8cf01fc6ff7f1d535d7e560a626349de1&oauth2_token_id=1138343922","released_at":1551452400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/761471249_590x270.jpg","collection":"server-side-swift","media_duration":1599,"title":"Functional Dependencies","subscription_only":true,"number":141,"url":"https:\/\/talk.objc.io\/episodes\/S01E141-functional-dependencies","synopsis":"We show a refactoring of the view code in the Swift Talk backend that allows us to pass around dependencies automatically using a functional pattern."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/758836751.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/318967143.m3u8?s=03495cac3403cbd89036748020cee7430e5dad2c&oauth2_token_id=1138343922","released_at":1550847600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/758836751_590x270.jpg","collection":"server-side-swift","media_duration":1259,"title":"The Swift Talk Backend (Part 3)","subscription_only":true,"number":140,"url":"https:\/\/talk.objc.io\/episodes\/S01E140-the-swift-talk-backend-part-3","synopsis":"To finish up the team member signup, we use a CSRF-validated POST request and then write a test for what we've built."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/755465327.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/317456644.m3u8?s=2cdbfbbed9791e4f1fb0fa3b4181e2fb204b0bab&oauth2_token_id=1138343922","released_at":1550242800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/755465327_590x270.jpg","collection":"server-side-swift","media_duration":1540,"title":"The Swift Talk Backend (Part 2)","subscription_only":true,"number":139,"url":"https:\/\/talk.objc.io\/episodes\/S01E139-the-swift-talk-backend-part-2","synopsis":"We continue working on the team member signup feature, showing how we handle sessions and perform database queries."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/755467987.jpg","released_at":1549638000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/755467987_590x270.jpg","collection":"server-side-swift","hls_url":"https:\/\/player.vimeo.com\/external\/313999121.m3u8?s=9f9386503f7dfc243e26134d8b442014e91c48c9&oauth2_token_id=1138343922","media_duration":1520,"title":"The Swift Talk Backend (Part 1)","subscription_only":false,"number":138,"url":"https:\/\/talk.objc.io\/episodes\/S01E138-the-swift-talk-backend-part-1","synopsis":"We show our new Swift Talk backend built on top of SwiftNIO by implementing a new team member signup feature."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/754642303.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/314778871.m3u8?s=ae80c13cc0f9d6c7bea286d4113b9061c0598199&oauth2_token_id=1138343922","released_at":1549033200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/754642303_590x270.jpg","collection":"networking","media_duration":1479,"title":"Testing Networking Code","subscription_only":true,"number":137,"url":"https:\/\/talk.objc.io\/episodes\/S01E137-testing-networking-code","synopsis":"We implement a test URL session that we can use to fake network responses."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/753560517.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/313347170.m3u8?s=4cf86885a01b2b193942bfb28b33bea7dcbc22f2&oauth2_token_id=1138343922","released_at":1548428400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/753560517_590x270.jpg","collection":"networking","media_duration":1208,"title":"Combined Resources with Futures","subscription_only":true,"number":136,"url":"https:\/\/talk.objc.io\/episodes\/S01E136-combined-resources-with-futures","synopsis":"We show an alternative implementation of combined resources that uses futures."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/750868879.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/312111472.m3u8?s=0b875fca21e9fa35ba6951e4650daa97b1c372f0&oauth2_token_id=1138343922","released_at":1547823600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/750868879_590x270.jpg","collection":"networking","media_duration":1330,"title":"Combined Resources (Part 2)","subscription_only":true,"number":135,"url":"https:\/\/talk.objc.io\/episodes\/S01E135-combined-resources-part-2","synopsis":"We add zip to the combined resource type, allowing us to express resources with multiple requests in parallel."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/750845156.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/310788602.m3u8?s=2a4ce5fdf7ab0910fb7b1d32569e3a7d15620bb4&oauth2_token_id=1138343922","released_at":1547218800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/750845156_590x270.jpg","collection":"networking","media_duration":1364,"title":"Combined Resources (Part 1)","subscription_only":true,"number":134,"url":"https:\/\/talk.objc.io\/episodes\/S01E134-combined-resources-part-1","synopsis":"We implement an abstraction on top of our tiny networking library to express resources consisting of multiple requests."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/749353427.jpg","released_at":1546614000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/749353427_590x270.jpg","collection":"networking","hls_url":"https:\/\/player.vimeo.com\/external\/309059915.m3u8?s=f9f04b9fe627b60370f0126d326e11485e935392&oauth2_token_id=1138343922","media_duration":1317,"title":"Tiny Networking Library Revisited","subscription_only":false,"number":133,"url":"https:\/\/talk.objc.io\/episodes\/S01E133-tiny-networking-library-revisited","synopsis":"We revisit the networking library we built in the first episode and discuss improvements we've made over the years."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/746110882.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/307679498.m3u8?s=876c7227948610489fcbd661d63b86f192357eb2&oauth2_token_id=1138343922","released_at":1545404400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/746110882_590x270.jpg","collection":"map-routing","media_duration":1846,"title":"Dijkstra's Shortest Path Algorithm","subscription_only":true,"number":132,"url":"https:\/\/talk.objc.io\/episodes\/S01E132-dijkstra-s-shortest-path-algorithm","synopsis":"We implement Dijkstra's algorithm to find the shortest path between any two points in the trail network."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/743279296.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/306394823.m3u8?s=13e41ea1370fb31ecefdac6807e4bbb4285fa269&oauth2_token_id=1138343922","released_at":1544799600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/743279296_590x270.jpg","collection":"map-routing","media_duration":1275,"title":"Performance Optimizations","subscription_only":true,"number":131,"url":"https:\/\/talk.objc.io\/episodes\/S01E131-performance-optimizations","synopsis":"We work on the efficiency of our graph-building algorithm, improving it by more than an order of magnitude."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/744446134.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/305054029.m3u8?s=3269333d4e252c7b92bc0717bd7202561db508a2&oauth2_token_id=1138343922","released_at":1544194800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/744446134_590x270.jpg","collection":"map-routing","media_duration":1118,"title":"Building the Graph (Part 2)","subscription_only":true,"number":130,"url":"https:\/\/talk.objc.io\/episodes\/S01E130-building-the-graph-part-2","synopsis":"We improve the graph-building algorithm by detecting overlaps between the tracks."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/741655635.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/303732792.m3u8?s=8ff5b673ee1050dd26f842ad4c9ed506f735ecc9&oauth2_token_id=1138343922","released_at":1543590000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/741655635_590x270.jpg","collection":"map-routing","media_duration":1912,"title":"Building the Graph (Part 1)","subscription_only":true,"number":129,"url":"https:\/\/talk.objc.io\/episodes\/S01E129-building-the-graph-part-1","synopsis":"We start implementing the algorithm that transforms the GPS points into a graph, which we then visualize for debugging purposes."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/739679550.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/302788828.m3u8?s=42f2df19d2984144c7117fc96c88151ebcf28e49&oauth2_token_id=1138343922","released_at":1542985200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/739679550_590x270.jpg","collection":"map-routing","media_duration":1605,"title":"Shortest Distance from Point to Line","subscription_only":true,"number":128,"url":"https:\/\/talk.objc.io\/episodes\/S01E128-shortest-distance-from-point-to-line","synopsis":"We start implementing the routing logic by looking at the problem of calculating the shortest distance from a point to a line."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/737791619.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/301160464.m3u8?s=394b06a4811b9b2d0fb086a96e3ce9543aead5fe&oauth2_token_id=1138343922","released_at":1542380400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/737791619_590x270.jpg","collection":"map-routing","media_duration":1541,"title":"Selecting Points on Tracks","subscription_only":true,"number":127,"url":"https:\/\/talk.objc.io\/episodes\/S01E127-selecting-points-on-tracks","synopsis":"We enable the selection of points on our running tracks by tapping on the map, and we finish up by factoring a lot of this code out of the view controller."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/737235208.jpg","released_at":1541775600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/737235208_590x270.jpg","collection":"map-routing","hls_url":"https:\/\/player.vimeo.com\/external\/299165636.m3u8?s=9d1122561507bac42f612a5d4b5007fe56a68487&oauth2_token_id=1138343922","media_duration":1145,"title":"Rendering Tracks","subscription_only":false,"number":126,"url":"https:\/\/talk.objc.io\/episodes\/S01E126-rendering-tracks","synopsis":"We show the routing app we'll build in this series and take the first steps by rendering track polygons on a map."},{"synopsis":"We show how the layout library we've built over the past two months can be used to adapt to any font and screen size.","poster_url":"https:\/\/i.vimeocdn.com\/video\/733862073.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/304120699.m3u8?s=cae2100dd85ef8466ec7d4a23644da541f334810&oauth2_token_id=1138343922","released_at":1541170800,"hls_url":"https:\/\/player.vimeo.com\/external\/296433724.m3u8?s=122cfa315581d75fb1ff62c10c9402ef804cc466&oauth2_token_id=1138343922","collection":"building-a-layout-library","media_duration":1244,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/733862073_590x270.jpg","title":"Building a Responsive Layout","subscription_only":false,"url":"https:\/\/talk.objc.io\/episodes\/S01E125-building-a-responsive-layout","number":125},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/733295682.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/304120678.m3u8?s=c02a912ae7c161fbd362008e95cb30d863fbecb6&oauth2_token_id=1138343922","released_at":1540562400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/733295682_590x270.jpg","collection":"building-a-layout-library","media_duration":1161,"title":"Flexible Boxes","subscription_only":true,"number":124,"url":"https:\/\/talk.objc.io\/episodes\/S01E124-flexible-boxes","synopsis":"We implement the flexible width option for nested layouts."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746438.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/296439635.m3u8?s=5c4fe1db928dca86a59d0b52e39330f7efb2e7cc&oauth2_token_id=1138343922","released_at":1539957600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746438_590x270.jpg","collection":"building-a-layout-library","media_duration":1211,"title":"Margins and Backgrounds","subscription_only":true,"number":123,"url":"https:\/\/talk.objc.io\/episodes\/S01E123-margins-and-backgrounds","synopsis":"We build upon the nested layout feature from last time to support layout margins, backgrounds, and more."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746490.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/296439608.m3u8?s=1480a93769e47e2fa417b3a0a492948571c1955d&oauth2_token_id=1138343922","released_at":1539352800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746490_590x270.jpg","collection":"building-a-layout-library","media_duration":1428,"title":"Nested Layouts","subscription_only":true,"number":122,"url":"https:\/\/talk.objc.io\/episodes\/S01E122-nested-layouts","synopsis":"We build a feature that allows us to create more complex layouts by nesting layouts within each other."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746570.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294638349.m3u8?s=289fc303aa92933c7e07b151ff214629e7f9caf2&oauth2_token_id=1138343922","released_at":1538748000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746570_590x270.jpg","collection":"building-a-layout-library","media_duration":936,"title":"Flexible Spaces","subscription_only":true,"number":121,"url":"https:\/\/talk.objc.io\/episodes\/S01E121-flexible-spaces","synopsis":"We add flexible spaces to our layout library and show how elements can be shown or hidden depending on the available space."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746656.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294637510.m3u8?s=0bbc143f7ba2c2637250b43f0bb38aea47b658f0&oauth2_token_id=1138343922","released_at":1538143200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746656_590x270.jpg","collection":"building-a-layout-library","media_duration":1495,"title":"Refactoring for Efficiency & Upcoming Features","subscription_only":true,"number":120,"url":"https:\/\/talk.objc.io\/episodes\/S01E120-refactoring-for-efficiency-upcoming-features","synopsis":"We refactor our code to remove duplication, improve efficiency, and enable features like flexible spacing."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746686.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294634441.m3u8?s=63c6e1c9ce0cf9d9f1ebf32892057e83c6bb3f6a&oauth2_token_id=1138343922","released_at":1537538400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746686_590x270.jpg","collection":"building-a-layout-library","media_duration":1465,"title":"Spacing & Better Syntax","subscription_only":true,"number":119,"url":"https:\/\/talk.objc.io\/episodes\/S01E119-spacing-better-syntax","synopsis":"We add some features to our layout library — starting with horizontal and vertical spacing — along with a better syntax to define layouts."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746743.jpg","released_at":1536933600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746743_590x270.jpg","collection":"building-a-layout-library","hls_url":"https:\/\/player.vimeo.com\/external\/294632666.m3u8?s=5559cfe596f0c46c999d7bd7f6d77f4148f7092c&oauth2_token_id=1138343922","media_duration":1878,"title":"Introduction & Prototype","subscription_only":false,"number":118,"url":"https:\/\/talk.objc.io\/episodes\/S01E118-introduction-prototype","synopsis":"We start building a responsive layout library that makes it easy to create layouts for all screen and font sizes."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746779.jpg","released_at":1536328800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746779_590x270.jpg","collection":"building-a-form-library","hls_url":"https:\/\/player.vimeo.com\/external\/294628590.m3u8?s=3f42ee1976b1607b55e8f623b2714f355acb3aaa&oauth2_token_id=1138343922","media_duration":895,"title":"Showing & Hiding Sections","subscription_only":false,"number":117,"url":"https:\/\/talk.objc.io\/episodes\/S01E117-showing-hiding-sections","synopsis":"Using a simple key path API, we add the ability to control the visibility of sections by any condition."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746804.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294626008.m3u8?s=52396bd6d9d7bf3343b6c475a9a2871becd38935&oauth2_token_id=1138343922","released_at":1535724000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746804_590x270.jpg","collection":"server-side-swift","media_duration":1338,"title":"Building a Custom XML Decoder (Part 2)","subscription_only":true,"number":116,"url":"https:\/\/talk.objc.io\/episodes\/S01E116-building-a-custom-xml-decoder-part-2","synopsis":"We add support for arrays by implementing an unkeyed decoding container and use custom decoding logic for dates and URLs."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746871.jpg","released_at":1535119200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746871_590x270.jpg","collection":"server-side-swift","hls_url":"https:\/\/player.vimeo.com\/external\/294624911.m3u8?s=ad19177093c6f1696c99a4c3611be8d93979383b&oauth2_token_id=1138343922","media_duration":1486,"title":"Building a Custom XML Decoder","subscription_only":false,"number":115,"url":"https:\/\/talk.objc.io\/episodes\/S01E115-building-a-custom-xml-decoder","synopsis":"We implement a custom XML decoder that allows us to decode responses from an XML API using Decodable."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746929.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294623505.m3u8?s=6e2601ccbbb31047ba2ff23732650ff7bb9b747d&oauth2_token_id=1138343922","released_at":1534514400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746929_590x270.jpg","collection":"server-side-swift","media_duration":1408,"title":"Reflection with Mirror and Decodable","subscription_only":true,"number":114,"url":"https:\/\/talk.objc.io\/episodes\/S01E114-reflection-with-mirror-and-decodable","synopsis":"We're using Swift's Mirror and Decodable APIs to generate database queries for structs in our Swift Talk backend project."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746953.jpg","released_at":1533909600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746953_590x270.jpg","collection":"building-a-form-library","hls_url":"https:\/\/player.vimeo.com\/external\/294622752.m3u8?s=40387c071f9fad0c9b26aac283942024baf6a486&oauth2_token_id=1138343922","media_duration":1665,"title":"Text Fields, Multi-Select, and Nested Forms","subscription_only":false,"number":113,"url":"https:\/\/talk.objc.io\/episodes\/S01E113-text-fields-multi-select-and-nested-forms","synopsis":"We return to the form library project and add several features to simplify common tasks."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747011.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294621923.m3u8?s=ee3e2544faacf89e9c8ec2aac2ba88fd4273ce8b&oauth2_token_id=1138343922","released_at":1533304800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747011_590x270.jpg","collection":"tooling","media_duration":1558,"title":"iOS Remote Debugger: The Network Framework","subscription_only":true,"number":112,"url":"https:\/\/talk.objc.io\/episodes\/S01E112-ios-remote-debugger-the-network-framework","synopsis":"We use Apple's new Network framework to simplify our own code."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747055.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294621214.m3u8?s=0d126ddad51b0e13b0c0d60b8eebdf1c20392826&oauth2_token_id=1138343922","released_at":1532700000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747055_590x270.jpg","collection":"tooling","media_duration":1775,"title":"iOS Remote Debugger: Receiving Data","subscription_only":true,"number":111,"url":"https:\/\/talk.objc.io\/episodes\/S01E111-ios-remote-debugger-receiving-data","synopsis":"We implement a JSON over TCP decoder to enable the debug client to receive data from the Mac app."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747111.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294620449.m3u8?s=9d07b33c23bcae3d73e67420206cb13d465e168d&oauth2_token_id=1138343922","released_at":1532095200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747111_590x270.jpg","collection":"tooling","media_duration":1358,"title":"iOS Remote Debugger: Sending Data","subscription_only":true,"number":110,"url":"https:\/\/talk.objc.io\/episodes\/S01E110-ios-remote-debugger-sending-data","synopsis":"We create a class that encapsulates the complexities of sending data via an output stream."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747135.jpg","released_at":1531490400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747135_590x270.jpg","collection":"tooling","hls_url":"https:\/\/player.vimeo.com\/external\/294619830.m3u8?s=2e1daffb9ad64d9f007a21a689376644c48f8616&oauth2_token_id=1138343922","media_duration":1710,"title":"iOS Remote Debugger: Connecting with Bonjour","subscription_only":false,"number":109,"url":"https:\/\/talk.objc.io\/episodes\/S01E109-ios-remote-debugger-connecting-with-bonjour","synopsis":"We're building a remote view state debugger, starting with the networking code on the client."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747199.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294618885.m3u8?s=76d868c13d833a0d50150c2759b3e122e948dde1&oauth2_token_id=1138343922","released_at":1530885600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747199_590x270.jpg","collection":"refactoring-large-view-controllers","media_duration":1646,"title":"Extracting View Code","subscription_only":true,"number":108,"url":"https:\/\/talk.objc.io\/episodes\/S01E108-extracting-view-code","synopsis":"In the last episode of this series, we factor out view code from the large view controller into a custom view class."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747263.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294618009.m3u8?s=e7dda878dc9a8c689898442b44b5b3efcaa1ddbc&oauth2_token_id=1138343922","released_at":1530280800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747263_590x270.jpg","collection":"refactoring-large-view-controllers","media_duration":1791,"title":"Child View Controllers (2)","subscription_only":true,"number":107,"url":"https:\/\/talk.objc.io\/episodes\/S01E107-child-view-controllers-2","synopsis":"We use the child view controller we created last time to factor out more code from the large view controller."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747336.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294616959.m3u8?s=a39f1559b0a0c049efefd1d00f637f2075503b00&oauth2_token_id=1138343922","released_at":1529676000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747336_590x270.jpg","collection":"refactoring-large-view-controllers","media_duration":1586,"title":"Child View Controllers","subscription_only":true,"number":106,"url":"https:\/\/talk.objc.io\/episodes\/S01E106-child-view-controllers","synopsis":"We extract a child view controller to further slim down our large view controller, making sure the code keeps compiling throughout the process."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747358.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294615264.m3u8?s=bf9f88073b9472aed2e6b98f65716d9da5c01054&oauth2_token_id=1138343922","released_at":1529071200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747358_590x270.jpg","collection":"refactoring-large-view-controllers","media_duration":1672,"title":"Extracting Networking Code","subscription_only":true,"number":105,"url":"https:\/\/talk.objc.io\/episodes\/S01E105-extracting-networking-code","synopsis":"We refactor networking-related code out of the view controller, separating networking and data transformation logic from UI code in the process."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747452.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294615248.m3u8?s=7cae90e7adb47f2dea1209002acdb6c6708a86b7&oauth2_token_id=1138343922","released_at":1528466400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747452_590x270.jpg","collection":"refactoring-large-view-controllers","media_duration":1130,"title":"Extracting Model Code","subscription_only":true,"number":104,"url":"https:\/\/talk.objc.io\/episodes\/S01E104-extracting-model-code","synopsis":"We extract Core Data-related code from the large view controller and move it into the model layer."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747474.jpg","released_at":1527861600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747474_590x270.jpg","collection":"refactoring-large-view-controllers","hls_url":"https:\/\/player.vimeo.com\/external\/294614693.m3u8?s=850de51a99385cd0c232cb4340a2d9c879ff7d2d&oauth2_token_id=1138343922","media_duration":1571,"title":"Extracting Pure Functions","subscription_only":false,"number":103,"url":"https:\/\/talk.objc.io\/episodes\/S01E103-extracting-pure-functions","synopsis":"We begin refactoring a large view controller from the Wikipedia iOS app by pulling pieces of helper code out as pure functions."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747513.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294612925.m3u8?s=15243562bcead1c6320f43944822608d229b53c0&oauth2_token_id=1138343922","released_at":1527256800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747513_590x270.jpg","collection":"building-a-form-library","media_duration":1468,"title":"Declarative Syntax","subscription_only":true,"number":102,"url":"https:\/\/talk.objc.io\/episodes\/S01E102-declarative-syntax","synopsis":"After finishing the cleanup from the last episode, we refactor our forms API to be even more succinct and declarative."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747564.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294612432.m3u8?s=60d51d2e7d7733b26962ec7601eeb705d86e8157&oauth2_token_id=1138343922","released_at":1526652000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747564_590x270.jpg","collection":"building-a-form-library","media_duration":1622,"title":"Cell and Section Helpers","subscription_only":true,"number":101,"url":"https:\/\/talk.objc.io\/episodes\/S01E101-cell-and-section-helpers","synopsis":"We create helper functions for form cells and sections, which simplify managing references and propagating updates."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747591.jpg","released_at":1526121060,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747591_590x270.jpg","collection":"experiments","hls_url":"https:\/\/player.vimeo.com\/external\/294611886.m3u8?s=f3b15388ad1c4ce31574c56670a94a7eb09ea45b&oauth2_token_id=1138343922","media_duration":2920,"title":"100th Episode Live Q&A","subscription_only":false,"number":100,"url":"https:\/\/talk.objc.io\/episodes\/S01E100-100th-episode-live-q-a","synopsis":"In our 100th episode we take questions from our viewers!"},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747640.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294610097.m3u8?s=c935e0c5bca660317b8a6953884288db5708b347&oauth2_token_id=1138343922","released_at":1525442400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747640_590x270.jpg","collection":"building-a-form-library","media_duration":1742,"title":"Creating Reusable Components","subscription_only":true,"number":99,"url":"https:\/\/talk.objc.io\/episodes\/S01E99-creating-reusable-components","synopsis":"We extract reusable toggle switch and text field components from our forms code and do more cleaning up in the process."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747697.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294609304.m3u8?s=9c8d045119f0b2bf7a51f7f1faed059f17155cd7&oauth2_token_id=1138343922","released_at":1524837600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747697_590x270.jpg","collection":"building-a-form-library","media_duration":1303,"title":"Cleaning Up the Code","subscription_only":true,"number":98,"url":"https:\/\/talk.objc.io\/episodes\/S01E98-cleaning-up-the-code","synopsis":"After refactoring in past episodes, it's time for some housekeeping: we clean up our form code and make it more reusable with generics."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747757.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294608678.m3u8?s=fdb39fcfda7223ab0b875d7c3ceabb6c189510c6&oauth2_token_id=1138343922","released_at":1524232800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747757_590x270.jpg","collection":"building-a-form-library","media_duration":1829,"title":"Creating a Reusable Form Driver","subscription_only":true,"number":97,"url":"https:\/\/talk.objc.io\/episodes\/S01E97-creating-a-reusable-form-driver","synopsis":"We refactor the form driver class to be reusable and define the entire form in a simple function."},{"synopsis":"We continue refactoring our forms code by creating a form table view controller as the first reusable component.","poster_url":"https:\/\/i.vimeocdn.com\/video\/731747814.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294607106.m3u8?s=fe8e04ae9cbb94d7cbb16bfbae279e5439c4258f&oauth2_token_id=1138343922","released_at":1523628000,"hls_url":"https:\/\/player.vimeo.com\/external\/294607073.m3u8?s=8587e25afcdf7dc4fed361864b389bc8f3103b73&oauth2_token_id=1138343922","collection":"building-a-form-library","media_duration":1220,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747814_590x270.jpg","title":"Extracting a Reusable Form View Controller","subscription_only":false,"url":"https:\/\/talk.objc.io\/episodes\/S01E96-extracting-a-reusable-form-view-controller","number":96},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747851.jpg","released_at":1523023200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747851_590x270.jpg","collection":"building-a-form-library","hls_url":"https:\/\/player.vimeo.com\/external\/294606227.m3u8?s=573af5d6e340279d46a9e4a5df21cbf8b3ce52d9&oauth2_token_id=1138343922","media_duration":1312,"title":"Simplifying IndexPath Logic","subscription_only":false,"number":95,"url":"https:\/\/talk.objc.io\/episodes\/S01E95-simplifying-indexpath-logic","synopsis":"We begin to refactor the imperative table view code from the last episode, working toward a more declarative approach of defining our form."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747919.jpg","released_at":1522422000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747919_590x270.jpg","collection":"building-a-form-library","hls_url":"https:\/\/player.vimeo.com\/external\/294604487.m3u8?s=7b3cb4b2ebd51e067ac958842ebd529ad6bdef89&oauth2_token_id=1138343922","media_duration":1496,"title":"Introduction","subscription_only":false,"number":94,"url":"https:\/\/talk.objc.io\/episodes\/S01E94-introduction","synopsis":"This episode marks the beginning of a new series where we refactor a hand-coded settings form into a reusable, declarative form library. In this episode, we build the base version and discuss the design goals of the library."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731747988.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294603387.m3u8?s=c1dfb07b0d42ccd38096b15ffa095370c30bf368&oauth2_token_id=1138343922","released_at":1521817200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731747988_590x270.jpg","collection":"swift-the-language","media_duration":1363,"title":"Handling Optionals","subscription_only":true,"number":93,"url":"https:\/\/talk.objc.io\/episodes\/S01E93-handling-optionals","synopsis":"We discuss many considerations and techniques for working with optionals."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748015.jpg","released_at":1521212400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748015_590x270.jpg","collection":"swift-the-language","hls_url":"https:\/\/player.vimeo.com\/external\/294601767.m3u8?s=3e1839c785644f4d63f726334c04c450c45c794d&oauth2_token_id=1138343922","media_duration":1341,"title":"Practicing with Pointers","subscription_only":false,"number":92,"url":"https:\/\/talk.objc.io\/episodes\/S01E92-practicing-with-pointers","synopsis":"We use Swift's pointer APIs to read a text file and split it into lines without using Swift's collection and string types."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748042.jpg","released_at":1520607600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748042_590x270.jpg","collection":"libraries","media_duration":1424,"title":"Rendering Markdown with Syntax Highlighting","subscription_only":true,"number":91,"url":"https:\/\/talk.objc.io\/episodes\/S01E91-rendering-markdown-with-syntax-highlighting","synopsis":"We extend a basic Markdown library using protocol composition to add support for syntax highlighting in Swift code blocks."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748101.jpg","released_at":1520002800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748101_590x270.jpg","collection":"swift-the-language","hls_url":"https:\/\/player.vimeo.com\/external\/294599908.m3u8?s=1abdda2cb76c351184c877f0d1148e3e189a200b&oauth2_token_id=1138343922","media_duration":1319,"title":"Concurrent Map","subscription_only":false,"number":90,"url":"https:\/\/talk.objc.io\/episodes\/S01E90-concurrent-map","synopsis":"We implement a concurrent version of the map method for arrays."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748136.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294597069.m3u8?s=bbf76cff3d88e98519ad505bac654d62231d4845&oauth2_token_id=1138343922","released_at":1519398000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748136_590x270.jpg","collection":"libraries","media_duration":1420,"title":"Extensible Libraries 2: Protocol Composition","subscription_only":true,"number":89,"url":"https:\/\/talk.objc.io\/episodes\/S01E89-extensible-libraries-2-protocol-composition","synopsis":"We show how protocol composition can be used to design extensible libraries, thereby solving the so-called \"Expression Problem.\""},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748197.jpg","released_at":1518793200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748197_590x270.jpg","collection":"libraries","hls_url":"https:\/\/player.vimeo.com\/external\/294597007.m3u8?s=af83e467cc19164f393968be5152052e2d5a8d53&oauth2_token_id=1138343922","media_duration":1344,"title":"Extensible Libraries 1: Enums vs Classes","subscription_only":false,"number":88,"url":"https:\/\/talk.objc.io\/episodes\/S01E88-extensible-libraries-1-enums-vs-classes","synopsis":"We discuss the capabilities and limitations of enums and classes when designing extensible libraries."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748249.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294596663.m3u8?s=ee156aca973eddffbfa72a7d5afa03c1f8ce3e2a&oauth2_token_id=1138343922","released_at":1518188400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748249_590x270.jpg","collection":"architecture","media_duration":1449,"title":"Sharing State between View Controllers in MVC (Part 2)","subscription_only":true,"number":87,"url":"https:\/\/talk.objc.io\/episodes\/S01E87-sharing-state-between-view-controllers-in-mvc-part-2","synopsis":"We continue implementing a mini player in the MVC variant of the sample app found in our App Architecture book."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748288.jpg","released_at":1517583600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748288_590x270.jpg","collection":"architecture","hls_url":"https:\/\/player.vimeo.com\/external\/294595092.m3u8?s=0e1f2ed1e332ceebcdc14f048c92f017de785496&oauth2_token_id=1138343922","media_duration":1165,"title":"Sharing State between View Controllers in MVC (Part 1)","subscription_only":false,"number":86,"url":"https:\/\/talk.objc.io\/episodes\/S01E86-sharing-state-between-view-controllers-in-mvc-part-1","synopsis":"We add a mini player to the MVC variant of the sample app found in our App Architecture book. We adjust our storyboard and discuss how to adapt the architecture."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748350.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294594281.m3u8?s=db7cd4e5631db20cd496f3bc0f8a78916faa6f5a&oauth2_token_id=1138343922","released_at":1516978800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748350_590x270.jpg","collection":"type-safe-api-wrappers","media_duration":2011,"title":"Wrapping libgit2","subscription_only":true,"number":85,"url":"https:\/\/talk.objc.io\/episodes\/S01E85-wrapping-libgit2","synopsis":"We write a wrapper around the libgit2 C library to work with Git repositories from macOS and iOS apps — and for the fun of using Swift's pointer APIs!"},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748371.jpg","released_at":1516381200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748371_590x270.jpg","collection":"architecture","hls_url":"https:\/\/player.vimeo.com\/external\/294592716.m3u8?s=19497d87c2abe547a75d2674dcab03d69a31e877&oauth2_token_id=1138343922","media_duration":281,"title":"Introducing Our New Book: App Architecture","subscription_only":false,"number":84,"url":"https:\/\/talk.objc.io\/episodes\/S01E84-introducing-our-new-book-app-architecture","synopsis":"Today we're releasing the early access edition of our new App Architecture book. We explain how it came about, what's in it, and how early access works."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748440.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294592525.m3u8?s=95eed4cd99691bbde34850a6782e453abc05e2ce&oauth2_token_id=1138343922","released_at":1515769200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748440_590x270.jpg","collection":"type-safe-api-wrappers","media_duration":1363,"title":"Wrapping Analytics APIs","subscription_only":true,"number":83,"url":"https:\/\/talk.objc.io\/episodes\/S01E83-wrapping-analytics-apis","synopsis":"We look at different techniques for wrapping analytics APIs in Swift and discuss their advantages and disadvantages."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748477.jpg","released_at":1515164400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748477_590x270.jpg","collection":"functional-programming","hls_url":"https:\/\/player.vimeo.com\/external\/294591328.m3u8?s=7960bd3f3ac3b7bf69a312fc0e38a89c753f7a53&oauth2_token_id=1138343922","media_duration":1120,"title":"Refactoring Imperative Layout Code","subscription_only":false,"number":82,"url":"https:\/\/talk.objc.io\/episodes\/S01E82-refactoring-imperative-layout-code","synopsis":"We refactor a simple flow layout to have a functional interface, disentangling the layout code from UIKit code."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748552.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294589987.m3u8?s=4879a4caf268badc95c40f2137b471e57882371f&oauth2_token_id=1138343922","released_at":1513954800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748552_590x270.jpg","collection":"incremental-programming","media_duration":2147,"title":"Reference Cycles and Ownership","subscription_only":true,"number":81,"url":"https:\/\/talk.objc.io\/episodes\/S01E81-reference-cycles-and-ownership","synopsis":"We use Xcode's memory debugger to resolve all the reference cycles in our glitch-free reactive code and introduce a proper ownership model."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731748581.jpg","released_at":1513350000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731748581_590x270.jpg","collection":"swift-the-language","hls_url":"https:\/\/player.vimeo.com\/external\/294586951.m3u8?s=3c6e5a751fcc27fd097cb6ea2f50413a8ebb7014&oauth2_token_id=1138343922","media_duration":802,"title":"Swift String vs. NSString","subscription_only":false,"number":80,"url":"https:\/\/talk.objc.io\/episodes\/S01E80-swift-string-vs-nsstring","synopsis":"We look at how to work with ranges in a mixed Swift String\/NSString environment."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731749425.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294586504.m3u8?s=52d464da06eaa2ae3c6722a1a0b816ee3fecb9d3&oauth2_token_id=1138343922","released_at":1512745200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731749425_590x270.jpg","collection":"swift-the-language","media_duration":1508,"title":"String Parsing Performance","subscription_only":true,"number":79,"url":"https:\/\/talk.objc.io\/episodes\/S01E79-string-parsing-performance","synopsis":"We benchmark the CSV parsing code from the previous episode and refactor it to become an order of magnitude faster."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731749466.jpg","released_at":1512140400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731749466_590x270.jpg","collection":"swift-the-language","hls_url":"https:\/\/player.vimeo.com\/external\/294585532.m3u8?s=29667572f0119071eb0134176ce3698855f5a287&oauth2_token_id=1138343922","media_duration":1708,"title":"Swift Strings and Substrings","subscription_only":false,"number":78,"url":"https:\/\/talk.objc.io\/episodes\/S01E78-swift-strings-and-substrings","synopsis":"We write a simple CSV parser as an example demonstrating how to work with Swift's String and Substring types."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731749554.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294584321.m3u8?s=03e5a9ff2f36c12f17b29e87addaf6c10248bfc1&oauth2_token_id=1138343922","released_at":1511535600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731749554_590x270.jpg","collection":"incremental-programming","media_duration":1495,"title":"A Technique to Avoid Reactive Glitches","subscription_only":true,"number":77,"url":"https:\/\/talk.objc.io\/episodes\/S01E77-a-technique-to-avoid-reactive-glitches","synopsis":"We refactor the simple reactive library from the last episode using topological sorting to avoid any temporarily wrong values."},{"synopsis":"We look at an example of a reactive pipeline with surprising behavior, discuss why it occurs, and how it could be improved.","poster_url":"https:\/\/i.vimeocdn.com\/video\/731749611.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294583265.m3u8?s=991a3c12da65b350f4e6769141879479a755af94&oauth2_token_id=1138343922","released_at":1510930800,"hls_url":"https:\/\/player.vimeo.com\/external\/294583210.m3u8?s=178c4ff0538bb8d1d69e281d99f92ad6a1d9222c&oauth2_token_id=1138343922","collection":"incremental-programming","media_duration":1096,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731749611_590x270.jpg","title":"Understanding Reactive Glitches","subscription_only":false,"url":"https:\/\/talk.objc.io\/episodes\/S01E76-understanding-reactive-glitches","number":76},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731749649.jpg","released_at":1510326000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731749649_590x270.jpg","collection":"swift-the-language","hls_url":"https:\/\/player.vimeo.com\/external\/294582498.m3u8?s=391d06f0d47bc50027c033348ba5c8b2224a4743&oauth2_token_id=1138343922","media_duration":1045,"title":"Auto Layout with Key Paths","subscription_only":false,"number":75,"url":"https:\/\/talk.objc.io\/episodes\/S01E75-auto-layout-with-key-paths","synopsis":"We clean up our layout code by introducing helper functions that leverage Swift's key paths."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731749702.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294581974.m3u8?s=dddff9cea241bb469c65144a5467c2841553ba65&oauth2_token_id=1138343922","released_at":1509721200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731749702_590x270.jpg","collection":"architecture","media_duration":1269,"title":"View Bindings in Pure Swift (Part 2)","subscription_only":true,"number":74,"url":"https:\/\/talk.objc.io\/episodes\/S01E74-view-bindings-in-pure-swift-part-2","synopsis":"We continue to expand our experimental view binding mechanism to implement dark mode in our app."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731749723.jpg","released_at":1509116400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731749723_590x270.jpg","collection":"architecture","hls_url":"https:\/\/player.vimeo.com\/external\/294581146.m3u8?s=e914f9a26507f123719bce484d18dadf34bdf2be&oauth2_token_id=1138343922","media_duration":1332,"title":"View Bindings in Pure Swift","subscription_only":false,"number":73,"url":"https:\/\/talk.objc.io\/episodes\/S01E73-view-bindings-in-pure-swift","synopsis":"We experiment with reactive view bindings that don't rely on runtime programming."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731749781.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294580341.m3u8?s=3812216aba3817f8bd421fbdc578d9b02ac95c13&oauth2_token_id=1138343922","released_at":1508511600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731749781_590x270.jpg","collection":"architecture","media_duration":2011,"title":"Adding a Custom View to a View-State Driven App","subscription_only":true,"number":72,"url":"https:\/\/talk.objc.io\/episodes\/S01E72-adding-a-custom-view-to-a-view-state-driven-app","synopsis":"We introduce a project we're going to work on over a few episodes. To get familiar with the code, we build a new feature using the app's view-state driven approach."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731749806.jpg","released_at":1507906800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731749806_590x270.jpg","collection":"swift-the-language","hls_url":"https:\/\/player.vimeo.com\/external\/294579031.m3u8?s=924de6fca59b7a20a7108b28808759674f8d4091&oauth2_token_id=1138343922","media_duration":1454,"title":"Type-Safe File Paths with Phantom Types","subscription_only":false,"number":71,"url":"https:\/\/talk.objc.io\/episodes\/S01E71-type-safe-file-paths-with-phantom-types","synopsis":"Brandon Kase joins us to show how Swift's type system can be leveraged to check file paths at compile time."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731749870.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294578143.m3u8?s=7f51c0bb799b242c10376869dffbb3a64cd1dd69&oauth2_token_id=1138343922","released_at":1507302000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731749870_590x270.jpg","collection":"table-views","media_duration":926,"title":"Table View Animations with Reactive Arrays","subscription_only":true,"number":70,"url":"https:\/\/talk.objc.io\/episodes\/S01E70-table-view-animations-with-reactive-arrays","synopsis":"We use the reactive array type from episodes #67 and #69 to back a table view. This allows us to correctly animate changes in the underlying data, even with filter and sort transformations applied."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731749914.jpg","released_at":1506697200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731749914_590x270.jpg","collection":"experiments","hls_url":"https:\/\/player.vimeo.com\/external\/294577549.m3u8?s=9bb38852301e3e556fa108052212b2ce7ba8888e&oauth2_token_id=1138343922","media_duration":1855,"title":"Reactive Data Structures: Arrays","subscription_only":false,"number":69,"url":"https:\/\/talk.objc.io\/episodes\/S01E69-reactive-data-structures-arrays","synopsis":"We build a reactive array type on top of the reactive list from episode #67 and implement a filter method."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750218.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294576500.m3u8?s=e7a0657c27f247ec2681cce79d20790ffee23108&oauth2_token_id=1138343922","released_at":1506092400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750218_590x270.jpg","collection":"architecture","media_duration":1499,"title":"The Elm Architecture (Part 2)","subscription_only":true,"number":68,"url":"https:\/\/talk.objc.io\/episodes\/S01E68-the-elm-architecture-part-2","synopsis":"We extend our Elm-style app with a more dynamic view hierarchy by adding a navigation controller and a table view."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750268.jpg","released_at":1505487600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750268_590x270.jpg","collection":"experiments","hls_url":"https:\/\/player.vimeo.com\/external\/294575702.m3u8?s=3d998388d8800be4c23b53597e96963835ac917b&oauth2_token_id=1138343922","media_duration":1558,"title":"Reactive Data Structures: Linked Lists","subscription_only":false,"number":67,"url":"https:\/\/talk.objc.io\/episodes\/S01E67-reactive-data-structures-linked-lists","synopsis":"We build a reactive linked list on top of reactive programming primitives. We implement a reduce method on this type, which does the minimum amount of work when the underlying data changes."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750389.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294574951.m3u8?s=3eeeb3028106e22b0c7e0f7892cd4f0a6a32e9f0&oauth2_token_id=1138343922","released_at":1504882800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750389_590x270.jpg","collection":"architecture","media_duration":999,"title":"The Elm Architecture (Part 1)","subscription_only":true,"number":66,"url":"https:\/\/talk.objc.io\/episodes\/S01E66-the-elm-architecture-part-1","synopsis":"We refactor our reducer-based project from episode #62 to use The Elm Architecture. Instead of interacting with UIKit directly, we build a virtual view hierarchy and let our Elm framework do the rest."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750425.jpg","released_at":1504278000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750425_590x270.jpg","collection":"data-structures","hls_url":"https:\/\/player.vimeo.com\/external\/294574404.m3u8?s=feacde5bfe51e2ea06eeb273aa2d56aa3557ebcc&oauth2_token_id=1138343922","media_duration":1678,"title":"Playground QuickLook for Binary Trees","subscription_only":false,"number":65,"url":"https:\/\/talk.objc.io\/episodes\/S01E65-playground-quicklook-for-binary-trees","synopsis":"We create a custom Quick Look extension to visualize binary tree structures in playgrounds."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750489.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294573778.m3u8?s=16ba2383b36c2c62ef7e3fa90ad4bbfeffce5138&oauth2_token_id=1138343922","released_at":1503673200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750489_590x270.jpg","collection":"table-views","media_duration":1824,"title":"Driving Table View Animations","subscription_only":true,"number":64,"url":"https:\/\/talk.objc.io\/episodes\/S01E64-driving-table-view-animations","synopsis":"We build a component similar to NSFetchedResultsController to decouple our view data and to drive table view animations."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750521.jpg","released_at":1503068400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750521_590x270.jpg","collection":"experiments","hls_url":"https:\/\/player.vimeo.com\/external\/294572908.m3u8?s=65aeb7939439a3cef19d5895b3dd96708f4a6603&oauth2_token_id=1138343922","media_duration":1379,"title":"Mutable Shared Structs (Part 2)","subscription_only":false,"number":63,"url":"https:\/\/talk.objc.io\/episodes\/S01E63-mutable-shared-structs-part-2","synopsis":"We refine the observation capabilities of our new data type."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750588.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294572287.m3u8?s=9c1859cfe773fff0e73ac59a574014325e82abf0&oauth2_token_id=1138343922","released_at":1502463600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750588_590x270.jpg","collection":"architecture","media_duration":1860,"title":"Testable View Controllers with Reducers","subscription_only":true,"number":62,"url":"https:\/\/talk.objc.io\/episodes\/S01E62-testable-view-controllers-with-reducers","synopsis":"We show the reducer pattern to simplify state management and to make typical view controller code more testable."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750610.jpg","released_at":1501858800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750610_590x270.jpg","collection":"experiments","hls_url":"https:\/\/player.vimeo.com\/external\/294571380.m3u8?s=d57c96e7f92005baf321d361e7dda96cb71de8b2&oauth2_token_id=1138343922","media_duration":1810,"title":"Mutable Shared Structs (Part 1)","subscription_only":false,"number":61,"url":"https:\/\/talk.objc.io\/episodes\/S01E61-mutable-shared-structs-part-1","synopsis":"We recap the tradeoffs between classes and structs and start implementation of our new type, leveraging Swift 4's keypaths."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750671.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294570688.m3u8?s=828f1e71a2ac50c78e6ccbe3d4068ada76955df4&oauth2_token_id=1138343922","released_at":1501254000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750671_590x270.jpg","collection":"tooling","media_duration":864,"title":"Xcode 9 Productivity Tips","subscription_only":true,"number":60,"url":"https:\/\/talk.objc.io\/episodes\/S01E60-xcode-9-productivity-tips","synopsis":"We show some of our favorite new productivity features in Xcode 9."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750707.jpg","released_at":1500649200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750707_590x270.jpg","collection":"tooling","hls_url":"https:\/\/player.vimeo.com\/external\/294570283.m3u8?s=e7e97f3b1a76594be8c2bdf43612a1feff48b586&oauth2_token_id=1138343922","media_duration":754,"title":"Refactoring with Xcode 9","subscription_only":false,"number":59,"url":"https:\/\/talk.objc.io\/episodes\/S01E59-refactoring-with-xcode-9","synopsis":"We take a look at features like renaming, extracting expressions, extracting methods, and more."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750730.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294569191.m3u8?s=c64b0e462f7003c6f7ba0fe40762908dd0a15947&oauth2_token_id=1138343922","released_at":1500044400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750730_590x270.jpg","collection":"data-structures","media_duration":1911,"title":"Red-Black Trees","subscription_only":true,"number":58,"url":"https:\/\/talk.objc.io\/episodes\/S01E58-red-black-trees","synopsis":"Building on the binary search tree code from episode #56, we implement red-black trees as self-balancing tree data structures and benchmark their performance."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750798.jpg","released_at":1499439600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750798_590x270.jpg","collection":"","hls_url":"https:\/\/player.vimeo.com\/external\/294569138.m3u8?s=e432bd41e8b9b7a02eecc65ee35ecba7dd577dad&oauth2_token_id=1138343922","media_duration":1471,"title":"Certificate Pinning","subscription_only":false,"number":57,"url":"https:\/\/talk.objc.io\/episodes\/S01E57-certificate-pinning","synopsis":"Today we're joined by Rob Napier, who explains why and how to add certificate pinning to your app."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750866.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294568528.m3u8?s=7ebb74a6a3db94da1311fc4b5a13f1d7ae46acda&oauth2_token_id=1138343922","released_at":1498834800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750866_590x270.jpg","collection":"data-structures","media_duration":1620,"title":"Binary Search Trees","subscription_only":true,"number":56,"url":"https:\/\/talk.objc.io\/episodes\/S01E56-binary-search-trees","synopsis":"We look at binary search trees as an alternative to last episode's sorted array implementation. We benchmark the performance of insertion and lookup in both data structures, with some surprising results."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750905.jpg","released_at":1498230000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750905_590x270.jpg","collection":"data-structures","hls_url":"https:\/\/player.vimeo.com\/external\/294567641.m3u8?s=c51e1a75a629bbf8a266a415c48aaa02e1a6c29a&oauth2_token_id=1138343922","media_duration":1611,"title":"Sorted Arrays with Binary Search","subscription_only":false,"number":55,"url":"https:\/\/talk.objc.io\/episodes\/S01E55-sorted-arrays-with-binary-search","synopsis":"Together with Károly, we improve our sorted array implementation using binary search. We benchmark both implementations to learn about their real-world performance."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731750982.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294566887.m3u8?s=3774aa5e515c306653bafe3da5458790ce0c7484&oauth2_token_id=1138343922","released_at":1497625200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731750982_590x270.jpg","collection":"architecture","media_duration":987,"title":"UI Elements with Callbacks","subscription_only":true,"number":54,"url":"https:\/\/talk.objc.io\/episodes\/S01E54-ui-elements-with-callbacks","synopsis":"We write a dedicated target-action to make it easier to augment existing UI controls with callbacks."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751022.jpg","released_at":1497020400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751022_590x270.jpg","collection":"ios-at-kickstarter","hls_url":"https:\/\/player.vimeo.com\/external\/294566400.m3u8?s=774d5b2522ea3162856d1a1d2a23f8fa86e28d85&oauth2_token_id=1138343922","media_duration":2327,"title":"Test-Driven Reactive Programming","subscription_only":false,"number":53,"url":"https:\/\/talk.objc.io\/episodes\/S01E53-test-driven-reactive-programming","synopsis":"Lisa from Kickstarter shows us their test-driven development process to reactive programming."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751067.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294565056.m3u8?s=c5827949e93abc4b2dc0031f61cf99afa6d370a5&oauth2_token_id=1138343922","released_at":1496415600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751067_590x270.jpg","collection":"swift-the-language","media_duration":709,"title":"Deleting Code with Swift 4","subscription_only":true,"number":52,"url":"https:\/\/talk.objc.io\/episodes\/S01E52-deleting-code-with-swift-4","synopsis":"Swift 4's new features lets us delete code we've written in previous episodes."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751132.jpg","released_at":1495810800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751132_590x270.jpg","collection":"ios-at-kickstarter","hls_url":"https:\/\/player.vimeo.com\/external\/294565015.m3u8?s=e9ac6dbe0a7def66b807a21058385b28ad46c38e&oauth2_token_id=1138343922","media_duration":1283,"title":"Playground-Driven Development","subscription_only":false,"number":51,"url":"https:\/\/talk.objc.io\/episodes\/S01E51-playground-driven-development","synopsis":"Brandon from Kickstarter demos how the company uses playgrounds to prototype and style individual view controllers."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751168.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294564566.m3u8?s=7bea18b4d202b0a1a7725cd1951ac2556f7bf88c&oauth2_token_id=1138343922","released_at":1495206000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751168_590x270.jpg","collection":"server-side-swift","media_duration":1514,"title":"Interfacing with PostgreSQL (Part 2)","subscription_only":true,"number":50,"url":"https:\/\/talk.objc.io\/episodes\/S01E50-interfacing-with-postgresql-part-2","synopsis":"We extend our libpq wrapper to handle queries with properly escaped parameters. To achieve this, we have to dive deep into Swift's unsafe pointer APIs."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751221.jpg","released_at":1494601200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751221_590x270.jpg","collection":"ios-at-kickstarter","hls_url":"https:\/\/player.vimeo.com\/external\/294563412.m3u8?s=0f27aca34073bfb1c64067be2fb327e07ef0bd05&oauth2_token_id=1138343922","media_duration":1631,"title":"Deep Linking","subscription_only":false,"number":49,"url":"https:\/\/talk.objc.io\/episodes\/S01E49-deep-linking","synopsis":"Brandon from Kickstarter shows their approach of unifying all potential entry points into an iOS app using a common route enum, both in a simple demo implementaion and in their open source code base."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751254.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294562334.m3u8?s=693990e80d731fa4e61044d660dea90a5fcd433a&oauth2_token_id=1138343922","released_at":1493996400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751254_590x270.jpg","collection":"server-side-swift","media_duration":1730,"title":"Interfacing with PostgreSQL","subscription_only":true,"number":48,"url":"https:\/\/talk.objc.io\/episodes\/S01E48-interfacing-with-postgresql","synopsis":"We implement a lightweight wrapper around the libpq C library."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751310.jpg","released_at":1493391600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751310_590x270.jpg","collection":"ios-at-kickstarter","hls_url":"https:\/\/player.vimeo.com\/external\/294560541.m3u8?s=f15dc7b0e4c3f222abebae858ab4773ba77c5b6b&oauth2_token_id=1138343922","media_duration":2038,"title":"View Models","subscription_only":false,"number":47,"url":"https:\/\/talk.objc.io\/episodes\/S01E47-view-models","synopsis":"Brandon from Kickstarter shows us how they write highly testable code with view models. We integrate Apple Pay payments and look at their open-source codebase."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751375.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294559523.m3u8?s=44b2278113af54b95e4c57f49fb1f5e3bcee2852&oauth2_token_id=1138343922","released_at":1492786800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751375_590x270.jpg","collection":"swift-the-language","media_duration":959,"title":"Combined Class and Protocol Requirements","subscription_only":true,"number":46,"url":"https:\/\/talk.objc.io\/episodes\/S01E46-combined-class-and-protocol-requirements","synopsis":"We look at multiple ways to create variables that have a class type but also conform to a protocol."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751410.jpg","released_at":1492182000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751410_590x270.jpg","collection":"server-side-swift","hls_url":"https:\/\/player.vimeo.com\/external\/294559035.m3u8?s=2f07b5c1d73048c4da83c54fe811e6f3124439a0&oauth2_token_id=1138343922","media_duration":1367,"title":"Routing","subscription_only":false,"number":45,"url":"https:\/\/talk.objc.io\/episodes\/S01E45-routing","synopsis":"We implement a type safe and Swift-like routing infrastructure that's pretty different from the common approach of most web frameworks."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751480.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294558430.m3u8?s=c87365aff2fa9b47ac3864846d7fbb7be2bdb221&oauth2_token_id=1138343922","released_at":1491577200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751480_590x270.jpg","collection":"server-side-swift","media_duration":1473,"title":"Setting Up a Server-Side Swift Project","subscription_only":true,"number":44,"url":"https:\/\/talk.objc.io\/episodes\/S01E44-setting-up-a-server-side-swift-project","synopsis":"We set up our development environment using the Swift package manager and Docker."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751522.jpg","released_at":1490972400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751522_590x270.jpg","collection":"architecture","hls_url":"https:\/\/player.vimeo.com\/external\/294557807.m3u8?s=72581ec6b729898bec79f6973b1a4bd51d534704&oauth2_token_id=1138343922","media_duration":1012,"title":"View Controller Refactoring","subscription_only":false,"number":43,"url":"https:\/\/talk.objc.io\/episodes\/S01E43-view-controller-refactoring","synopsis":"Instead of letting multiple view controllers manage the navigation bar's state individually, we pull this code out and unify the logic in one place."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751552.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294556830.m3u8?s=fa4c18d4d54c4bd06b4dac7ceace06f0fd93d15d&oauth2_token_id=1138343922","released_at":1490367600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751552_590x270.jpg","collection":"reactive-programming","media_duration":1416,"title":"Thread Safety","subscription_only":true,"number":42,"url":"https:\/\/talk.objc.io\/episodes\/S01E42-thread-safety","synopsis":"We make our Signal implementation thread-safe by safeguarding the access to shared resources."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746087.jpg","released_at":1489762800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746087_590x270.jpg","collection":"collection-protocols","hls_url":"https:\/\/player.vimeo.com\/external\/294533928.m3u8?s=68cd320570342d9895519cf0992b163788a28484&oauth2_token_id=1138343922","media_duration":1011,"title":"Conforming IndexSet to Collection","subscription_only":false,"number":41,"url":"https:\/\/talk.objc.io\/episodes\/S01E41-conforming-indexset-to-collection","synopsis":"To conform IndexSet to the Collection protocol we implement a custom index type along the way."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751705.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294556789.m3u8?s=754f733e7f0d6a1b7207169bfc12a296294b29c5&oauth2_token_id=1138343922","released_at":1489158000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751705_590x270.jpg","collection":"reactive-programming","media_duration":988,"title":"Signal Ownership and Subscriptions","subscription_only":true,"number":40,"url":"https:\/\/talk.objc.io\/episodes\/S01E40-signal-ownership-and-subscriptions","synopsis":"We add the ability to map over signals and control subscriptions in a more fine-grained manner. Along the way, we improve the signal ownership model and implement the concept of disposables."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751744.jpg","released_at":1488553200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751744_590x270.jpg","collection":"reactive-programming","hls_url":"https:\/\/player.vimeo.com\/external\/294556081.m3u8?s=0c688d1ef2b5d6f9ff88442da6e8f1836a4e374b&oauth2_token_id=1138343922","media_duration":1425,"title":"From Futures to Signals","subscription_only":false,"number":39,"url":"https:\/\/talk.objc.io\/episodes\/S01E39-from-futures-to-signals","synopsis":"We extend the Future type of a previous episode to a simple reactive library. Along the way, we dive into debugging a reference cycle in our implementation."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751802.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294555284.m3u8?s=9506cfcc9b99da51c117c12f2223fe0817476675&oauth2_token_id=1138343922","released_at":1487948400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751802_590x270.jpg","collection":"collection-protocols","media_duration":848,"title":"Conforming IndexSet to Sequence","subscription_only":true,"number":38,"url":"https:\/\/talk.objc.io\/episodes\/S01E38-conforming-indexset-to-sequence","synopsis":"Conforming to the Sequence protocol allows us to efficiently iterate over the elements, and we gain all of its useful functionality."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751873.jpg","released_at":1487343600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751873_590x270.jpg","collection":"collection-protocols","hls_url":"https:\/\/player.vimeo.com\/external\/294554968.m3u8?s=534d6b1374c75de9d9347d8b4d38a7d6d5231cd3&oauth2_token_id=1138343922","media_duration":1132,"title":"Building a Custom IndexSet Collection","subscription_only":false,"number":37,"url":"https:\/\/talk.objc.io\/episodes\/S01E37-building-a-custom-indexset-collection","synopsis":"We build the basics for a custom index set collection type."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751931.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294554517.m3u8?s=f041555bfedb1f0cffa1711a4c92c3ff6f2fd538&oauth2_token_id=1138343922","released_at":1486738800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751931_590x270.jpg","collection":"reactive-programming","media_duration":1050,"title":"Futures","subscription_only":true,"number":36,"url":"https:\/\/talk.objc.io\/episodes\/S01E36-futures","synopsis":"We implement a Futures type that we can use instead of callbacks as a first step towards a reactive library."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731751957.jpg","released_at":1486134000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731751957_590x270.jpg","collection":"collection-protocols","hls_url":"https:\/\/player.vimeo.com\/external\/294554120.m3u8?s=c7f96c0e491ff7c45232f9b3d94150fdacd738bf&oauth2_token_id=1138343922","media_duration":1002,"title":"Sorted Arrays","subscription_only":false,"number":35,"url":"https:\/\/talk.objc.io\/episodes\/S01E35-sorted-arrays","synopsis":"We build a sorted array type on top of Swift's native array and make it conform to the Collection protocol."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731752062.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294553659.m3u8?s=6a0617c287d88fc4e48aa8179521a7cf2594ec85&oauth2_token_id=1138343922","released_at":1485529200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731752062_590x270.jpg","collection":"reactive-programming","media_duration":1572,"title":"Reactive Programming","subscription_only":true,"number":34,"url":"https:\/\/talk.objc.io\/episodes\/S01E34-reactive-programming","synopsis":"We take a look at how reactive programming challenges us to think differently."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731752094.jpg","released_at":1484924400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731752094_590x270.jpg","collection":"collection-protocols","hls_url":"https:\/\/player.vimeo.com\/external\/294552953.m3u8?s=cdd5783a3066a79aeb37de1f892ba3edbd278d4c&oauth2_token_id=1138343922","media_duration":1014,"title":"Sequence & Iterator","subscription_only":false,"number":33,"url":"https:\/\/talk.objc.io\/episodes\/S01E33-sequence-iterator","synopsis":"We make our collection extension even more generic by implementing it on the Sequence protocol."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731752149.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294552571.m3u8?s=4ff71adef4a17ca122a71899c822c0dd1c433a38&oauth2_token_id=1138343922","released_at":1484319600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731752149_590x270.jpg","collection":"collection-protocols","media_duration":916,"title":"Array, ArraySlice & Collection","subscription_only":true,"number":32,"url":"https:\/\/talk.objc.io\/episodes\/S01E32-array-arrayslice-collection","synopsis":"We show how to use the Collection protocol to make an extension available not just on array, but on all collections."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731752184.jpg","released_at":1483714800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731752184_590x270.jpg","collection":"swift-the-language","hls_url":"https:\/\/player.vimeo.com\/external\/294551891.m3u8?s=7d9694fb259cbc132830da931dfd4f734eb6abeb&oauth2_token_id=1138343922","media_duration":914,"title":"Mutating Untyped Dictionaries","subscription_only":false,"number":31,"url":"https:\/\/talk.objc.io\/episodes\/S01E31-mutating-untyped-dictionaries","synopsis":"Mutating a nested untyped dictionary can be a challenge. To solve it we discuss the mutability of value types and the concept of l-values."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731752247.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294551055.m3u8?s=a648220d22a73402e5995990996a15cd68e2de7f&oauth2_token_id=1138343922","released_at":1483110000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731752247_590x270.jpg","collection":"","media_duration":967,"title":"Interfaces","subscription_only":true,"number":30,"url":"https:\/\/talk.objc.io\/episodes\/S01E30-interfaces","synopsis":"We talk about the importance of types and interfaces as tools to express your intent precisely and to set the proper boundaries."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731752713.jpg","released_at":1481900400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731752713_590x270.jpg","collection":"swift-the-language","hls_url":"https:\/\/player.vimeo.com\/external\/294550613.m3u8?s=4815484f0ed4d25e32379bc269e96718711b1453&oauth2_token_id=1138343922","media_duration":962,"title":"Protocols & Class Hierarchies","subscription_only":false,"number":29,"url":"https:\/\/talk.objc.io\/episodes\/S01E29-protocols-class-hierarchies","synopsis":"We refactor a class hierarchy using a protocol and discuss the differences between both approaches."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731752779.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294550226.m3u8?s=be334e9d1824de5fa570a910a49100dfad18959c&oauth2_token_id=1138343922","released_at":1481295600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731752779_590x270.jpg","collection":"type-safe-api-wrappers","media_duration":1082,"title":"Typed Notifications (Part 2)","subscription_only":true,"number":28,"url":"https:\/\/talk.objc.io\/episodes\/S01E28-typed-notifications-part-2","synopsis":"We extend our notification wrapper from episode #27 and discuss an alternative protocol-based approach."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731752818.jpg","released_at":1480690800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731752818_590x270.jpg","collection":"type-safe-api-wrappers","hls_url":"https:\/\/player.vimeo.com\/external\/294549779.m3u8?s=0a83a702196517dc515a07c2d46e639cba7936f3&oauth2_token_id=1138343922","media_duration":1072,"title":"Typed Notifications (Part 1)","subscription_only":false,"number":27,"url":"https:\/\/talk.objc.io\/episodes\/S01E27-typed-notifications-part-1","synopsis":"A lightweight generic wrapper around Foundation's notification API lets us avoid boilerplate code and provides a type-safe API."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731752867.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294549379.m3u8?s=881e8c1de5153afa1c161066041463bd52eb5118&oauth2_token_id=1138343922","released_at":1480086000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731752867_590x270.jpg","collection":"table-views","media_duration":1410,"title":"Generic Table View Controllers (Part 2)","subscription_only":true,"number":26,"url":"https:\/\/talk.objc.io\/episodes\/S01E26-generic-table-view-controllers-part-2","synopsis":"We build a generic, type-safe table view controller that can handle multiple cell classes."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731752888.jpg","released_at":1479481200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731752888_590x270.jpg","collection":"networking","hls_url":"https:\/\/player.vimeo.com\/external\/294548812.m3u8?s=9df16e37ef4f9d402c16d96aaec34743f546ba15&oauth2_token_id=1138343922","media_duration":1413,"title":"Adding Caching","subscription_only":false,"number":25,"url":"https:\/\/talk.objc.io\/episodes\/S01E25-adding-caching","synopsis":"We add support for caching network requests without altering our original networking abstraction."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731752951.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294548198.m3u8?s=61244085e4cbc8bf837815dcda746007fbd886c4&oauth2_token_id=1138343922","released_at":1478876400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731752951_590x270.jpg","collection":"architecture","media_duration":929,"title":"Delegates & Callbacks","subscription_only":true,"number":24,"url":"https:\/\/talk.objc.io\/episodes\/S01E24-delegates-callbacks","synopsis":"We discuss the pros and cons of delegates versus callback functions and why delegate protocols are always class only."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731752987.jpg","released_at":1478271600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731752987_590x270.jpg","collection":"swift-the-language","hls_url":"https:\/\/player.vimeo.com\/external\/294547833.m3u8?s=90d064ff163089694bb6648d7fe669837f492aff&oauth2_token_id=1138343922","media_duration":1447,"title":"Splitting Arrays","subscription_only":false,"number":23,"url":"https:\/\/talk.objc.io\/episodes\/S01E23-splitting-arrays","synopsis":"We talk about a familiar but surprisingly tricky problem: splitting an array into groups of elements. We discuss the pros and cons of our own solutions along with the solutions people sent us via Twitter!"},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753007.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294546844.m3u8?s=86bd40ac87819196e5ed5271ce9bec83e4b8e16b&oauth2_token_id=1138343922","released_at":1477666800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753007_590x270.jpg","collection":"","media_duration":1037,"title":"Command Line Tools with Swift","subscription_only":true,"number":22,"url":"https:\/\/talk.objc.io\/episodes\/S01E22-command-line-tools-with-swift","synopsis":"We show how we build simple command line tools leveraging the Cocoa frameworks. We use the Swift Package manager to include dependencies in our project."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753062.jpg","released_at":1477062000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753062_590x270.jpg","collection":"swift-the-language","hls_url":"https:\/\/player.vimeo.com\/external\/294546794.m3u8?s=af8cde693cfca94d6df7a1bbb091deff4242bea5&oauth2_token_id=1138343922","media_duration":967,"title":"Structs and Mutation","subscription_only":false,"number":21,"url":"https:\/\/talk.objc.io\/episodes\/S01E21-structs-and-mutation","synopsis":"We can change structs by mutation, functional chaining, and inout parameters. We discuss how they differ at the call site and why they’re all equivalent."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753085.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294545920.m3u8?s=a909420216705b155f418580e0db66a74ac1ca31&oauth2_token_id=1138343922","released_at":1476457200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753085_590x270.jpg","collection":"swift-the-language","media_duration":1131,"title":"Understanding Value Type Performance","subscription_only":true,"number":20,"url":"https:\/\/talk.objc.io\/episodes\/S01E20-understanding-value-type-performance","synopsis":"We use copy-on-write to write an efficient struct wrapper around NSMutableData and discuss how the standard library uses the same approach."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753134.jpg","released_at":1475852400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753134_590x270.jpg","collection":"functional-programming","hls_url":"https:\/\/player.vimeo.com\/external\/294545891.m3u8?s=e004583bd45cedd02213f4aa41bb012dc67d2474&oauth2_token_id=1138343922","media_duration":1385,"title":"From Runtime Programming to Functions","subscription_only":false,"number":19,"url":"https:\/\/talk.objc.io\/episodes\/S01E19-from-runtime-programming-to-functions","synopsis":"We build a flexible sort descriptor abstraction on top of Swift's native sort methods, which is dynamic and type safe."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753174.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294544811.m3u8?s=18c21fe62be75f536f9b1350d2bce46522a50dff&oauth2_token_id=1138343922","released_at":1475247600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753174_590x270.jpg","collection":"ledger-mac-app","media_duration":960,"title":"Adding Search","subscription_only":true,"number":18,"url":"https:\/\/talk.objc.io\/episodes\/S01E18-adding-search","synopsis":"We leverage the existing infrastructure of our app to add a search field with very little code."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753226.jpg","released_at":1474642800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753226_590x270.jpg","collection":"ledger-mac-app","hls_url":"https:\/\/player.vimeo.com\/external\/294544758.m3u8?s=a9ef78d3d8cc7dee15cd1bbab2c5ee7dbc22215f&oauth2_token_id=1138343922","media_duration":1111,"title":"Architecture","subscription_only":false,"number":17,"url":"https:\/\/talk.objc.io\/episodes\/S01E17-architecture","synopsis":"We connect multiple view controllers using a coordinator and callback functions. We simplify the control flow by refactoring the UI state into its own struct."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753306.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294544284.m3u8?s=57abb779b2027953f7a7a0319e6d2bbabc07f862&oauth2_token_id=1138343922","released_at":1474038000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753306_590x270.jpg","collection":"ledger-mac-app","media_duration":1290,"title":"Building Parser Combinators (Part 2)","subscription_only":true,"number":16,"url":"https:\/\/talk.objc.io\/episodes\/S01E16-building-parser-combinators-part-2","synopsis":"We implement some of the more challenging parts of parser combinators."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753338.jpg","released_at":1473433200,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753338_590x270.jpg","collection":"ledger-mac-app","hls_url":"https:\/\/player.vimeo.com\/external\/294543691.m3u8?s=65f8646b0764ae28660016a526556acfcb536e75&oauth2_token_id=1138343922","media_duration":982,"title":"Building Parser Combinators (Part 1)","subscription_only":false,"number":15,"url":"https:\/\/talk.objc.io\/episodes\/S01E15-building-parser-combinators-part-1","synopsis":"Join us in the functional programming gym to stretch your object-oriented comfort zone while we lay the groundwork for a parser combinator library."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753366.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294542452.m3u8?s=4efc30f19c16fde105cfef4a964e52bc2fb6a247&oauth2_token_id=1138343922","released_at":1472824800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753366_590x270.jpg","collection":"ledger-mac-app","media_duration":1691,"title":"Growing Trees with Classes and Structs","subscription_only":true,"number":14,"url":"https:\/\/talk.objc.io\/episodes\/S01E14-growing-trees-with-classes-and-structs","synopsis":"We build a tree structure from an array of Ledger account names. We first implement the tree using a class, and then we refactor it to a struct and discuss the differences and tradeoffs involved."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753421.jpg","released_at":1472220000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753421_590x270.jpg","collection":"ledger-mac-app","hls_url":"https:\/\/player.vimeo.com\/external\/294542384.m3u8?s=6f1a75eb4f482f8c9ea701d8b06e1cf478bdc68b&oauth2_token_id=1138343922","media_duration":1523,"title":"Parsing Techniques","subscription_only":false,"number":13,"url":"https:\/\/talk.objc.io\/episodes\/S01E13-parsing-techniques","synopsis":"We look at two different techniques to parse a simple expression language: handwritten parsers and parser combinators."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753452.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294540396.m3u8?s=b85449be07d21d56109b7cae60ab57a40c7e6b63&oauth2_token_id=1138343922","released_at":1471618800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753452_590x270.jpg","collection":"ledger-mac-app","media_duration":1543,"title":"Evaluating Transactions","subscription_only":true,"number":12,"url":"https:\/\/talk.objc.io\/episodes\/S01E12-evaluating-transactions","synopsis":"Writing the code for evaluating transactions required continuous refactoring to keep our code simple and clean."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753510.jpg","released_at":1471014000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753510_590x270.jpg","collection":"ledger-mac-app","hls_url":"https:\/\/player.vimeo.com\/external\/294540006.m3u8?s=e633243c6a7ea7bb12fdb1ff0424baa0cff13c03&oauth2_token_id=1138343922","media_duration":1776,"title":"Evaluating Expressions","subscription_only":false,"number":11,"url":"https:\/\/talk.objc.io\/episodes\/S01E11-evaluating-expressions","synopsis":"Expressions are at the heart of Ledger. We write an evaluator for this expression language in a test-driven way."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/790458441.jpg","released_at":1471013940,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/790458441_590x270.jpg","collection":"ledger-mac-app","hls_url":"https:\/\/player.vimeo.com\/external\/341756829.m3u8?s=8c56bdda98fd8fa22968ad7c8cd2d63336ce4936&oauth2_token_id=1138343922","media_duration":465,"title":"Introduction","subscription_only":false,"number":10,"url":"https:\/\/talk.objc.io\/episodes\/S01E10-introduction","synopsis":"We give a quick introduction to Ledger itself and to the Mac app we're going to build."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731753573.jpg","released_at":1470402000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731753573_590x270.jpg","collection":"","hls_url":"https:\/\/player.vimeo.com\/external\/294531751.m3u8?s=871b5b173046c43422af71d30acc6031937c4032&oauth2_token_id=1138343922","media_duration":834,"title":"Q&A","subscription_only":false,"number":9,"url":"https:\/\/talk.objc.io\/episodes\/S01E9-q-a","synopsis":"In this episode, we answer some of the questions we've received over the past few weeks. We cover networking, table views, stack views, our App class, and testing."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731745938.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294537253.m3u8?s=9545b518f2a4e9818dabf8f1c80ba59dd072ed83&oauth2_token_id=1138343922","released_at":1469800800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731745938_590x270.jpg","collection":"networking","media_duration":1120,"title":"Adding POST Requests","subscription_only":true,"number":8,"url":"https:\/\/talk.objc.io\/episodes\/S01E8-adding-post-requests","synopsis":"We add POST support to a simple networking layer, using Swift's enums with associated values and generics."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731745691.jpg","released_at":1469196000,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731745691_590x270.jpg","collection":"architecture","hls_url":"https:\/\/player.vimeo.com\/external\/294539566.m3u8?s=62635f8bcfd29ec49ad853cb2865b35735c9bfdd&oauth2_token_id=1138343922","media_duration":1303,"title":"Stack Views with Enums","subscription_only":false,"number":7,"url":"https:\/\/talk.objc.io\/episodes\/S01E7-stack-views-with-enums","synopsis":"We create an abstraction around stack views using enums to specify UI elements in a declarative style."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746199.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294533007.m3u8?s=2609792a56904a28083ccbad74a71a6da7ed6de8&oauth2_token_id=1138343922","released_at":1468587600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746199_590x270.jpg","collection":"table-views","media_duration":1182,"title":"Generic Table View Controllers","subscription_only":true,"number":6,"url":"https:\/\/talk.objc.io\/episodes\/S01E6-generic-table-view-controllers","synopsis":"We leverage Swift's generics to keep our table view controller code clean."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746253.jpg","released_at":1467982800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746253_590x270.jpg","collection":"architecture","hls_url":"https:\/\/player.vimeo.com\/external\/294532339.m3u8?s=5feaa1d0e5de45629b02be7a8ef5ea1ef5a5b02d&oauth2_token_id=1138343922","media_duration":1214,"title":"Connecting View Controllers","subscription_only":false,"number":5,"url":"https:\/\/talk.objc.io\/episodes\/S01E5-connecting-view-controllers","synopsis":"We refactor our code by moving the app's flow from the storyboard into a separate coordinator class. This avoids view controllers having implicit knowledge of their context."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731745802.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294538949.m3u8?s=bfa7e0beec8d5310c5495d763949dc421ead5896&oauth2_token_id=1138343922","released_at":1467379800,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731745802_590x270.jpg","collection":"functional-programming","media_duration":1113,"title":"Rendering CommonMark (Part 2)","subscription_only":true,"number":4,"url":"https:\/\/talk.objc.io\/episodes\/S01E4-rendering-commonmark-part-2","synopsis":"We add customizable styles to our CommonMark renderer from episode #2."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731746009.jpg","released_at":1466780400,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731746009_590x270.jpg","collection":"networking","hls_url":"https:\/\/player.vimeo.com\/external\/294534792.m3u8?s=0b8270d191286f1176394e906fc5ab330236bf78&oauth2_token_id=1138343922","media_duration":1310,"title":"Loading View Controllers","subscription_only":false,"number":3,"url":"https:\/\/talk.objc.io\/episodes\/S01E3-loading-view-controllers","synopsis":"We explore different approaches to factor out asynchronous loading code from view controllers, using protocols, container view controllers, and generics."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731745849.jpg","preview_url":"https:\/\/player.vimeo.com\/external\/294531864.m3u8?s=4f0bcb2b3cf0971fe43bb13460e7f5bd74eef80c&oauth2_token_id=1138343922","released_at":1466175600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731745849_590x270.jpg","collection":"functional-programming","media_duration":1395,"title":"Rendering CommonMark","subscription_only":true,"number":2,"url":"https:\/\/talk.objc.io\/episodes\/S01E2-rendering-commonmark","synopsis":"We create attributed strings from CommonMark. We continually refactor our code to make the central logic short and understandable."},{"poster_url":"https:\/\/i.vimeocdn.com\/video\/731745967.jpg","released_at":1466175600,"small_poster_url":"https:\/\/i.vimeocdn.com\/video\/731745967_590x270.jpg","collection":"networking","hls_url":"https:\/\/player.vimeo.com\/external\/294535770.m3u8?s=d653b1e547a54fd8abbe331554d1ed3b195915d7&oauth2_token_id=1138343922","media_duration":1369,"title":"Tiny Networking Library","subscription_only":false,"number":1,"url":"https:\/\/talk.objc.io\/episodes\/S01E1-tiny-networking-library","synopsis":"We make use of Swift's generics and structs to build a simple network layer with great testability."}] --------------------------------------------------------------------------------