├── .gitignore ├── Adding-Content-to-Apple-Music.xcodeproj ├── .xcodesamplecode.plist └── project.pbxproj ├── AppleMusicSample ├── AppDelegate.swift ├── Controllers │ ├── AppleMusicManager.swift │ ├── AuthorizationDataSource.swift │ ├── AuthorizationManager.swift │ └── MediaLibraryManager.swift ├── Info.plist ├── Model │ ├── Artwork.swift │ ├── MediaItem.swift │ └── SerializationError.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Assets.imageset │ │ │ ├── Assets.png │ │ │ ├── Assets@2x.png │ │ │ ├── Assets@3x.png │ │ │ └── Contents.json │ │ ├── Backward.imageset │ │ │ ├── Backward.pdf │ │ │ └── Contents.json │ │ ├── Configure.imageset │ │ │ ├── Configure.png │ │ │ ├── Configure@2x.png │ │ │ ├── Configure@3x.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Forward.imageset │ │ │ ├── Contents.json │ │ │ └── Forward.pdf │ │ ├── Pause.imageset │ │ │ ├── Contents.json │ │ │ └── Pause.pdf │ │ └── Play.imageset │ │ │ ├── Contents.json │ │ │ └── Play.pdf │ └── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard ├── Utility │ ├── AppleMusicRequestFactory.swift │ └── JSONKeys.swift └── Views │ ├── AuthorizationTableViewController.swift │ ├── MediaItemTableViewCell.swift │ └── MediaSearchTableViewController.swift ├── Configuration └── SampleCode.xcconfig ├── LICENSE └── LICENSE.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # See LICENSE folder for this sample’s licensing information. 2 | # 3 | # Apple sample code gitignore configuration. 4 | 5 | # Finder 6 | .DS_Store 7 | 8 | # Xcode - User files 9 | xcuserdata/ 10 | *.xcworkspace 11 | -------------------------------------------------------------------------------- /Adding-Content-to-Apple-Music.xcodeproj/.xcodesamplecode.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Adding-Content-to-Apple-Music.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4213F0AA1EC2CE250014E741 /* MediaItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4213F0A91EC2CE250014E741 /* MediaItem.swift */; }; 11 | 4213F0AC1EC2DCE40014E741 /* Artwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4213F0AB1EC2DCE40014E741 /* Artwork.swift */; }; 12 | 4213F0AE1EC2DF410014E741 /* SerializationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4213F0AD1EC2DF410014E741 /* SerializationError.swift */; }; 13 | 4213F0B01EC2E5A40014E741 /* MediaItemTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4213F0AF1EC2E5A40014E741 /* MediaItemTableViewCell.swift */; }; 14 | 4213F0B21EC383620014E741 /* AuthorizationDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4213F0B11EC383620014E741 /* AuthorizationDataSource.swift */; }; 15 | 4213F0B41EC3871B0014E741 /* AuthorizationTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4213F0B31EC3871B0014E741 /* AuthorizationTableViewController.swift */; }; 16 | 4249C3FB1EDB8D3000CC40AE /* JSONKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4249C3FA1EDB8D3000CC40AE /* JSONKeys.swift */; }; 17 | 424BE3E01EBAEAA60034DB00 /* AuthorizationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424BE3DD1EBAEAA60034DB00 /* AuthorizationManager.swift */; }; 18 | 424BE3E11EBAEAA60034DB00 /* MediaLibraryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424BE3DE1EBAEAA60034DB00 /* MediaLibraryManager.swift */; }; 19 | 424BE3E81EBAEAD40034DB00 /* MediaSearchTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424BE3E51EBAEAD40034DB00 /* MediaSearchTableViewController.swift */; }; 20 | 424BE3F01EBAEAFE0034DB00 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 424BE3EB1EBAEAFE0034DB00 /* Assets.xcassets */; }; 21 | 424BE3F11EBAEAFE0034DB00 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 424BE3EC1EBAEAFE0034DB00 /* LaunchScreen.storyboard */; }; 22 | 424BE3F21EBAEAFE0034DB00 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 424BE3EE1EBAEAFE0034DB00 /* Main.storyboard */; }; 23 | 424E9D661EBBC9F2006C2C82 /* AppleMusicManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424E9D651EBBC9F2006C2C82 /* AppleMusicManager.swift */; }; 24 | 42C9B1991EDF5D7B009A9DE7 /* AppleMusicRequestFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42C9B1981EDF5D7B009A9DE7 /* AppleMusicRequestFactory.swift */; }; 25 | 42EDDE671E9AE0B10012EDD0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42EDDE661E9AE0B10012EDD0 /* AppDelegate.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXFileReference section */ 29 | 4213F0A91EC2CE250014E741 /* MediaItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaItem.swift; sourceTree = ""; }; 30 | 4213F0AB1EC2DCE40014E741 /* Artwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Artwork.swift; sourceTree = ""; }; 31 | 4213F0AD1EC2DF410014E741 /* SerializationError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SerializationError.swift; sourceTree = ""; }; 32 | 4213F0AF1EC2E5A40014E741 /* MediaItemTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaItemTableViewCell.swift; sourceTree = ""; }; 33 | 4213F0B11EC383620014E741 /* AuthorizationDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationDataSource.swift; sourceTree = ""; }; 34 | 4213F0B31EC3871B0014E741 /* AuthorizationTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationTableViewController.swift; sourceTree = ""; }; 35 | 4249C3FA1EDB8D3000CC40AE /* JSONKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONKeys.swift; sourceTree = ""; }; 36 | 424BE3DD1EBAEAA60034DB00 /* AuthorizationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorizationManager.swift; sourceTree = ""; }; 37 | 424BE3DE1EBAEAA60034DB00 /* MediaLibraryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaLibraryManager.swift; sourceTree = ""; }; 38 | 424BE3E51EBAEAD40034DB00 /* MediaSearchTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaSearchTableViewController.swift; sourceTree = ""; }; 39 | 424BE3EB1EBAEAFE0034DB00 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 40 | 424BE3ED1EBAEAFE0034DB00 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 41 | 424BE3EF1EBAEAFE0034DB00 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 424E9D651EBBC9F2006C2C82 /* AppleMusicManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppleMusicManager.swift; sourceTree = ""; }; 43 | 42C9B1981EDF5D7B009A9DE7 /* AppleMusicRequestFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleMusicRequestFactory.swift; sourceTree = ""; }; 44 | 42D655431EC5466D00BB5656 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.markdown; }; 45 | 42EDDE631E9AE0B10012EDD0 /* Adding-Content-to-Apple-Music.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Adding-Content-to-Apple-Music.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 42EDDE661E9AE0B10012EDD0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 47 | 42EDDE721E9AE0B10012EDD0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | 72931F350C68CB630D4B87DB /* LICENSE.txt */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; 49 | F13FB93F1A3E73CB63454144 /* SampleCode.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = SampleCode.xcconfig; path = Configuration/SampleCode.xcconfig; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 42EDDE601E9AE0B10012EDD0 /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | /* End PBXFrameworksBuildPhase section */ 61 | 62 | /* Begin PBXGroup section */ 63 | 11CC06F8F87E2015D0058DB5 /* LICENSE */ = { 64 | isa = PBXGroup; 65 | children = ( 66 | 72931F350C68CB630D4B87DB /* LICENSE.txt */, 67 | ); 68 | path = LICENSE; 69 | sourceTree = ""; 70 | }; 71 | 4213F0A81EC2CDBE0014E741 /* Model */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 4213F0AB1EC2DCE40014E741 /* Artwork.swift */, 75 | 4213F0A91EC2CE250014E741 /* MediaItem.swift */, 76 | 4213F0AD1EC2DF410014E741 /* SerializationError.swift */, 77 | ); 78 | path = Model; 79 | sourceTree = ""; 80 | }; 81 | 4249C3F71EDB6AF100CC40AE /* Utility */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 4249C3FA1EDB8D3000CC40AE /* JSONKeys.swift */, 85 | 42C9B1981EDF5D7B009A9DE7 /* AppleMusicRequestFactory.swift */, 86 | ); 87 | path = Utility; 88 | sourceTree = ""; 89 | }; 90 | 424BE3DC1EBAEAA60034DB00 /* Controllers */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 424E9D651EBBC9F2006C2C82 /* AppleMusicManager.swift */, 94 | 424BE3DD1EBAEAA60034DB00 /* AuthorizationManager.swift */, 95 | 4213F0B11EC383620014E741 /* AuthorizationDataSource.swift */, 96 | 424BE3DE1EBAEAA60034DB00 /* MediaLibraryManager.swift */, 97 | ); 98 | path = Controllers; 99 | sourceTree = ""; 100 | }; 101 | 424BE3E31EBAEAD40034DB00 /* Views */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 4213F0B31EC3871B0014E741 /* AuthorizationTableViewController.swift */, 105 | 424BE3E51EBAEAD40034DB00 /* MediaSearchTableViewController.swift */, 106 | 4213F0AF1EC2E5A40014E741 /* MediaItemTableViewCell.swift */, 107 | ); 108 | path = Views; 109 | sourceTree = ""; 110 | }; 111 | 424BE3EA1EBAEAFE0034DB00 /* Resources */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | 424BE3EB1EBAEAFE0034DB00 /* Assets.xcassets */, 115 | 424BE3EC1EBAEAFE0034DB00 /* LaunchScreen.storyboard */, 116 | 424BE3EE1EBAEAFE0034DB00 /* Main.storyboard */, 117 | ); 118 | path = Resources; 119 | sourceTree = ""; 120 | }; 121 | 42EDDE5A1E9AE0B10012EDD0 = { 122 | isa = PBXGroup; 123 | children = ( 124 | 42D655431EC5466D00BB5656 /* README.md */, 125 | 42EDDE651E9AE0B10012EDD0 /* AppleMusicSample */, 126 | 42EDDE641E9AE0B10012EDD0 /* Products */, 127 | EF42C4662604E1E250B91451 /* Configuration */, 128 | 11CC06F8F87E2015D0058DB5 /* LICENSE */, 129 | ); 130 | sourceTree = ""; 131 | }; 132 | 42EDDE641E9AE0B10012EDD0 /* Products */ = { 133 | isa = PBXGroup; 134 | children = ( 135 | 42EDDE631E9AE0B10012EDD0 /* Adding-Content-to-Apple-Music.app */, 136 | ); 137 | name = Products; 138 | sourceTree = ""; 139 | }; 140 | 42EDDE651E9AE0B10012EDD0 /* AppleMusicSample */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 42EDDE661E9AE0B10012EDD0 /* AppDelegate.swift */, 144 | 424BE3DC1EBAEAA60034DB00 /* Controllers */, 145 | 4213F0A81EC2CDBE0014E741 /* Model */, 146 | 4249C3F71EDB6AF100CC40AE /* Utility */, 147 | 424BE3E31EBAEAD40034DB00 /* Views */, 148 | 424BE3EA1EBAEAFE0034DB00 /* Resources */, 149 | 42EDDE721E9AE0B10012EDD0 /* Info.plist */, 150 | ); 151 | path = AppleMusicSample; 152 | sourceTree = ""; 153 | }; 154 | EF42C4662604E1E250B91451 /* Configuration */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | F13FB93F1A3E73CB63454144 /* SampleCode.xcconfig */, 158 | ); 159 | name = Configuration; 160 | sourceTree = ""; 161 | }; 162 | /* End PBXGroup section */ 163 | 164 | /* Begin PBXNativeTarget section */ 165 | 42EDDE621E9AE0B10012EDD0 /* Adding-Content-to-Apple-Music */ = { 166 | isa = PBXNativeTarget; 167 | buildConfigurationList = 42EDDE751E9AE0B10012EDD0 /* Build configuration list for PBXNativeTarget "Adding-Content-to-Apple-Music" */; 168 | buildPhases = ( 169 | 42EDDE5F1E9AE0B10012EDD0 /* Sources */, 170 | 42EDDE601E9AE0B10012EDD0 /* Frameworks */, 171 | 42EDDE611E9AE0B10012EDD0 /* Resources */, 172 | ); 173 | buildRules = ( 174 | ); 175 | dependencies = ( 176 | ); 177 | name = "Adding-Content-to-Apple-Music"; 178 | productName = AppleMusicSample; 179 | productReference = 42EDDE631E9AE0B10012EDD0 /* Adding-Content-to-Apple-Music.app */; 180 | productType = "com.apple.product-type.application"; 181 | }; 182 | /* End PBXNativeTarget section */ 183 | 184 | /* Begin PBXProject section */ 185 | 42EDDE5B1E9AE0B10012EDD0 /* Project object */ = { 186 | isa = PBXProject; 187 | attributes = { 188 | LastSwiftUpdateCheck = 0830; 189 | LastUpgradeCheck = 0900; 190 | ORGANIZATIONNAME = Apple; 191 | TargetAttributes = { 192 | 42EDDE621E9AE0B10012EDD0 = { 193 | CreatedOnToolsVersion = 8.3.1; 194 | DevelopmentTeam = LNQ22YF8UB; 195 | LastSwiftMigration = 0900; 196 | ProvisioningStyle = Automatic; 197 | }; 198 | }; 199 | }; 200 | buildConfigurationList = 42EDDE5E1E9AE0B10012EDD0 /* Build configuration list for PBXProject "Adding-Content-to-Apple-Music" */; 201 | compatibilityVersion = "Xcode 3.2"; 202 | developmentRegion = English; 203 | hasScannedForEncodings = 0; 204 | knownRegions = ( 205 | en, 206 | Base, 207 | ); 208 | mainGroup = 42EDDE5A1E9AE0B10012EDD0; 209 | productRefGroup = 42EDDE641E9AE0B10012EDD0 /* Products */; 210 | projectDirPath = ""; 211 | projectRoot = ""; 212 | targets = ( 213 | 42EDDE621E9AE0B10012EDD0 /* Adding-Content-to-Apple-Music */, 214 | ); 215 | }; 216 | /* End PBXProject section */ 217 | 218 | /* Begin PBXResourcesBuildPhase section */ 219 | 42EDDE611E9AE0B10012EDD0 /* Resources */ = { 220 | isa = PBXResourcesBuildPhase; 221 | buildActionMask = 2147483647; 222 | files = ( 223 | 424BE3F21EBAEAFE0034DB00 /* Main.storyboard in Resources */, 224 | 424BE3F01EBAEAFE0034DB00 /* Assets.xcassets in Resources */, 225 | 424BE3F11EBAEAFE0034DB00 /* LaunchScreen.storyboard in Resources */, 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | }; 229 | /* End PBXResourcesBuildPhase section */ 230 | 231 | /* Begin PBXSourcesBuildPhase section */ 232 | 42EDDE5F1E9AE0B10012EDD0 /* Sources */ = { 233 | isa = PBXSourcesBuildPhase; 234 | buildActionMask = 2147483647; 235 | files = ( 236 | 4213F0B01EC2E5A40014E741 /* MediaItemTableViewCell.swift in Sources */, 237 | 424BE3E01EBAEAA60034DB00 /* AuthorizationManager.swift in Sources */, 238 | 4213F0B21EC383620014E741 /* AuthorizationDataSource.swift in Sources */, 239 | 424BE3E11EBAEAA60034DB00 /* MediaLibraryManager.swift in Sources */, 240 | 4249C3FB1EDB8D3000CC40AE /* JSONKeys.swift in Sources */, 241 | 4213F0AA1EC2CE250014E741 /* MediaItem.swift in Sources */, 242 | 4213F0AC1EC2DCE40014E741 /* Artwork.swift in Sources */, 243 | 424E9D661EBBC9F2006C2C82 /* AppleMusicManager.swift in Sources */, 244 | 424BE3E81EBAEAD40034DB00 /* MediaSearchTableViewController.swift in Sources */, 245 | 4213F0AE1EC2DF410014E741 /* SerializationError.swift in Sources */, 246 | 42EDDE671E9AE0B10012EDD0 /* AppDelegate.swift in Sources */, 247 | 4213F0B41EC3871B0014E741 /* AuthorizationTableViewController.swift in Sources */, 248 | 42C9B1991EDF5D7B009A9DE7 /* AppleMusicRequestFactory.swift in Sources */, 249 | ); 250 | runOnlyForDeploymentPostprocessing = 0; 251 | }; 252 | /* End PBXSourcesBuildPhase section */ 253 | 254 | /* Begin PBXVariantGroup section */ 255 | 424BE3EC1EBAEAFE0034DB00 /* LaunchScreen.storyboard */ = { 256 | isa = PBXVariantGroup; 257 | children = ( 258 | 424BE3ED1EBAEAFE0034DB00 /* Base */, 259 | ); 260 | name = LaunchScreen.storyboard; 261 | sourceTree = ""; 262 | }; 263 | 424BE3EE1EBAEAFE0034DB00 /* Main.storyboard */ = { 264 | isa = PBXVariantGroup; 265 | children = ( 266 | 424BE3EF1EBAEAFE0034DB00 /* Base */, 267 | ); 268 | name = Main.storyboard; 269 | sourceTree = ""; 270 | }; 271 | /* End PBXVariantGroup section */ 272 | 273 | /* Begin XCBuildConfiguration section */ 274 | 42EDDE731E9AE0B10012EDD0 /* Debug */ = { 275 | isa = XCBuildConfiguration; 276 | baseConfigurationReference = F13FB93F1A3E73CB63454144 /* SampleCode.xcconfig */; 277 | buildSettings = { 278 | ALWAYS_SEARCH_USER_PATHS = NO; 279 | CLANG_ANALYZER_NONNULL = YES; 280 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 281 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 282 | CLANG_CXX_LIBRARY = "libc++"; 283 | CLANG_ENABLE_MODULES = YES; 284 | CLANG_ENABLE_OBJC_ARC = YES; 285 | CLANG_WARN_BOOL_CONVERSION = YES; 286 | CLANG_WARN_CONSTANT_CONVERSION = YES; 287 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 288 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 289 | CLANG_WARN_EMPTY_BODY = YES; 290 | CLANG_WARN_ENUM_CONVERSION = YES; 291 | CLANG_WARN_INFINITE_RECURSION = YES; 292 | CLANG_WARN_INT_CONVERSION = YES; 293 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 294 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 295 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 296 | CLANG_WARN_UNREACHABLE_CODE = YES; 297 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 298 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 299 | COPY_PHASE_STRIP = NO; 300 | DEBUG_INFORMATION_FORMAT = dwarf; 301 | ENABLE_STRICT_OBJC_MSGSEND = YES; 302 | ENABLE_TESTABILITY = YES; 303 | GCC_C_LANGUAGE_STANDARD = gnu99; 304 | GCC_DYNAMIC_NO_PIC = NO; 305 | GCC_NO_COMMON_BLOCKS = YES; 306 | GCC_OPTIMIZATION_LEVEL = 0; 307 | GCC_PREPROCESSOR_DEFINITIONS = ( 308 | "DEBUG=1", 309 | "$(inherited)", 310 | ); 311 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 312 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 313 | GCC_WARN_UNDECLARED_SELECTOR = YES; 314 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 315 | GCC_WARN_UNUSED_FUNCTION = YES; 316 | GCC_WARN_UNUSED_VARIABLE = YES; 317 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 318 | MTL_ENABLE_DEBUG_INFO = YES; 319 | ONLY_ACTIVE_ARCH = YES; 320 | SDKROOT = iphoneos; 321 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 322 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 323 | TARGETED_DEVICE_FAMILY = "1,2"; 324 | }; 325 | name = Debug; 326 | }; 327 | 42EDDE741E9AE0B10012EDD0 /* Release */ = { 328 | isa = XCBuildConfiguration; 329 | baseConfigurationReference = F13FB93F1A3E73CB63454144 /* SampleCode.xcconfig */; 330 | buildSettings = { 331 | ALWAYS_SEARCH_USER_PATHS = NO; 332 | CLANG_ANALYZER_NONNULL = YES; 333 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 334 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 335 | CLANG_CXX_LIBRARY = "libc++"; 336 | CLANG_ENABLE_MODULES = YES; 337 | CLANG_ENABLE_OBJC_ARC = YES; 338 | CLANG_WARN_BOOL_CONVERSION = YES; 339 | CLANG_WARN_CONSTANT_CONVERSION = YES; 340 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 341 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 342 | CLANG_WARN_EMPTY_BODY = YES; 343 | CLANG_WARN_ENUM_CONVERSION = YES; 344 | CLANG_WARN_INFINITE_RECURSION = YES; 345 | CLANG_WARN_INT_CONVERSION = YES; 346 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 347 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 348 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 349 | CLANG_WARN_UNREACHABLE_CODE = YES; 350 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 351 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 352 | COPY_PHASE_STRIP = NO; 353 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 354 | ENABLE_NS_ASSERTIONS = NO; 355 | ENABLE_STRICT_OBJC_MSGSEND = YES; 356 | GCC_C_LANGUAGE_STANDARD = gnu99; 357 | GCC_NO_COMMON_BLOCKS = YES; 358 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 359 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 360 | GCC_WARN_UNDECLARED_SELECTOR = YES; 361 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 362 | GCC_WARN_UNUSED_FUNCTION = YES; 363 | GCC_WARN_UNUSED_VARIABLE = YES; 364 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 365 | MTL_ENABLE_DEBUG_INFO = NO; 366 | SDKROOT = iphoneos; 367 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 368 | TARGETED_DEVICE_FAMILY = "1,2"; 369 | VALIDATE_PRODUCT = YES; 370 | }; 371 | name = Release; 372 | }; 373 | 42EDDE761E9AE0B10012EDD0 /* Debug */ = { 374 | isa = XCBuildConfiguration; 375 | baseConfigurationReference = F13FB93F1A3E73CB63454144 /* SampleCode.xcconfig */; 376 | buildSettings = { 377 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 378 | DEVELOPMENT_TEAM = LNQ22YF8UB; 379 | INFOPLIST_FILE = AppleMusicSample/Info.plist; 380 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 381 | PRODUCT_BUNDLE_IDENTIFIER = music.com.hum; 382 | PRODUCT_NAME = "$(TARGET_NAME)"; 383 | PROVISIONING_PROFILE_SPECIFIER = ""; 384 | SWIFT_SWIFT3_OBJC_INFERENCE = Off; 385 | SWIFT_VERSION = 4.0; 386 | }; 387 | name = Debug; 388 | }; 389 | 42EDDE771E9AE0B10012EDD0 /* Release */ = { 390 | isa = XCBuildConfiguration; 391 | baseConfigurationReference = F13FB93F1A3E73CB63454144 /* SampleCode.xcconfig */; 392 | buildSettings = { 393 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 394 | DEVELOPMENT_TEAM = LNQ22YF8UB; 395 | INFOPLIST_FILE = AppleMusicSample/Info.plist; 396 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 397 | PRODUCT_BUNDLE_IDENTIFIER = music.com.hum; 398 | PRODUCT_NAME = "$(TARGET_NAME)"; 399 | PROVISIONING_PROFILE_SPECIFIER = ""; 400 | SWIFT_SWIFT3_OBJC_INFERENCE = Off; 401 | SWIFT_VERSION = 4.0; 402 | }; 403 | name = Release; 404 | }; 405 | /* End XCBuildConfiguration section */ 406 | 407 | /* Begin XCConfigurationList section */ 408 | 42EDDE5E1E9AE0B10012EDD0 /* Build configuration list for PBXProject "Adding-Content-to-Apple-Music" */ = { 409 | isa = XCConfigurationList; 410 | buildConfigurations = ( 411 | 42EDDE731E9AE0B10012EDD0 /* Debug */, 412 | 42EDDE741E9AE0B10012EDD0 /* Release */, 413 | ); 414 | defaultConfigurationIsVisible = 0; 415 | defaultConfigurationName = Release; 416 | }; 417 | 42EDDE751E9AE0B10012EDD0 /* Build configuration list for PBXNativeTarget "Adding-Content-to-Apple-Music" */ = { 418 | isa = XCConfigurationList; 419 | buildConfigurations = ( 420 | 42EDDE761E9AE0B10012EDD0 /* Debug */, 421 | 42EDDE771E9AE0B10012EDD0 /* Release */, 422 | ); 423 | defaultConfigurationIsVisible = 0; 424 | defaultConfigurationName = Release; 425 | }; 426 | /* End XCConfigurationList section */ 427 | }; 428 | rootObject = 42EDDE5B1E9AE0B10012EDD0 /* Project object */; 429 | } 430 | -------------------------------------------------------------------------------- /AppleMusicSample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | The Application's AppDelegate which configures the rest of the application's dependencies. 6 | */ 7 | 8 | import UIKit 9 | 10 | @UIApplicationMain 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | // MARK: Properties 14 | 15 | var window: UIWindow? 16 | 17 | /// The instance of `AuthorizationManager` which is responsible for managing authorization for the application. 18 | lazy var authorizationManager: AuthorizationManager = { 19 | return AuthorizationManager(appleMusicManager: self.appleMusicManager) 20 | }() 21 | 22 | /// The instance of `MediaLibraryManager` which manages the `MPPMediaPlaylist` this application creates. 23 | lazy var mediaLibraryManager: MediaLibraryManager = { 24 | return MediaLibraryManager(authorizationManager: self.authorizationManager) 25 | }() 26 | 27 | 28 | /// The instance of `AppleMusicManager` which handles making web service calls to Apple Music Web Services. 29 | var appleMusicManager = AppleMusicManager() 30 | 31 | // MARK: Application Life Cycle Methods 32 | 33 | func application(_ application: UIApplication, 34 | didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { 35 | 36 | guard let authorizationTableViewController = topViewControllerAtTabBarIndex(0) as? AuthorizationTableViewController else { 37 | fatalError("Unable to find expected \(AuthorizationTableViewController.self) in at TabBar Index 0") 38 | } 39 | 40 | guard let mediaSearchTableViewController = topViewControllerAtTabBarIndex(4) as? MediaSearchTableViewController else { 41 | fatalError("Unable to find expected \(MediaSearchTableViewController.self) in at TabBar Index 4") 42 | } 43 | 44 | authorizationTableViewController.authorizationManager = authorizationManager 45 | 46 | mediaSearchTableViewController.authorizationManager = authorizationManager 47 | mediaSearchTableViewController.mediaLibraryManager = mediaLibraryManager 48 | 49 | return true 50 | } 51 | 52 | 53 | // MARK: Utility Methods 54 | 55 | func topViewControllerAtTabBarIndex(_ index: Int) -> UIViewController? { 56 | guard let tabBarController = window?.rootViewController as? UITabBarController, 57 | let navigationController = tabBarController.viewControllers?[index] as? UINavigationController else { 58 | fatalError("Unable to find expected View Controller in Main.storyboard.") 59 | } 60 | 61 | return navigationController.topViewController 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /AppleMusicSample/Controllers/AppleMusicManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | `AppleMusicManager` manages creating making the Apple Music API calls for the `MediaSearchTableViewController`. 6 | */ 7 | 8 | import Foundation 9 | import StoreKit 10 | import UIKit 11 | 12 | class AppleMusicManager { 13 | 14 | // MARK: Types 15 | 16 | /// The completion handler that is called when an Apple Music Catalog Search API call completes. 17 | typealias CatalogSearchCompletionHandler = (_ mediaItems: [[MediaItem]], _ error: Error?) -> Void 18 | 19 | /// The completion handler that is called when an Apple Music Get User Storefront API call completes. 20 | typealias GetUserStorefrontCompletionHandler = (_ storefront: String?, _ error: Error?) -> Void 21 | 22 | // MARK: Properties 23 | 24 | /// The instance of `URLSession` that is going to be used for making network calls. 25 | lazy var urlSession: URLSession = { 26 | // Configure the `URLSession` instance that is going to be used for making network calls. 27 | let urlSessionConfiguration = URLSessionConfiguration.default 28 | 29 | return URLSession(configuration: urlSessionConfiguration) 30 | }() 31 | 32 | /// The storefront id that is used when making Apple Music API calls. 33 | var storefrontID: String? 34 | 35 | func fetchDeveloperToken() -> String? { 36 | 37 | // MARK: ADAPT: YOU MUST IMPLEMENT THIS METHOD 38 | let developerAuthenticationToken: String? = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ikw4RE05UjJDWVYifQ.eyJpc3MiOiJMTlEyMllGOFVCIiwiaWF0IjoxNTAyMjk2MjE4LCJleHAiOjE1MDIzMzk0MTh9.l7rUYrfD4JJ0XTfclJC8R990cu-YfJoM5yVFGXL0MwPPrjgx1aPFB-YGWU2R6pMknItAd7f_WhlwjmldBZlk7w" 39 | return developerAuthenticationToken 40 | } 41 | 42 | // MARK: General Apple Music API Methods 43 | 44 | func performAppleMusicCatalogSearch(with term: String, countryCode: String, completion: @escaping CatalogSearchCompletionHandler) { 45 | 46 | guard let developerToken = fetchDeveloperToken() else { 47 | fatalError("Developer Token not configured. See README for more details.") 48 | } 49 | 50 | let urlRequest = AppleMusicRequestFactory.createSearchRequest(with: term, countryCode: countryCode, developerToken: developerToken) 51 | 52 | let task = urlSession.dataTask(with: urlRequest) { (data, response, error) in 53 | guard error == nil, let urlResponse = response as? HTTPURLResponse, urlResponse.statusCode == 200 else { 54 | completion([], error) 55 | return 56 | } 57 | 58 | do { 59 | let mediaItems = try self.processMediaItemSections(from: data!) 60 | completion(mediaItems, nil) 61 | 62 | } catch { 63 | fatalError("An error occurred: \(error.localizedDescription)") 64 | } 65 | } 66 | 67 | task.resume() 68 | } 69 | 70 | func performAppleMusicStorefrontsLookup(regionCode: String, completion: @escaping GetUserStorefrontCompletionHandler) { 71 | guard let developerToken = fetchDeveloperToken() else { 72 | fatalError("Developer Token not configured. See README for more details.") 73 | } 74 | 75 | let urlRequest = AppleMusicRequestFactory.createStorefrontsRequest(regionCode: regionCode, developerToken: developerToken) 76 | 77 | let task = urlSession.dataTask(with: urlRequest) { [weak self] (data, response, error) in 78 | guard error == nil, let urlResponse = response as? HTTPURLResponse, urlResponse.statusCode == 200 else { 79 | completion(nil, error) 80 | return 81 | } 82 | 83 | do { 84 | let identifier = try self?.processStorefront(from: data!) 85 | completion(identifier, nil) 86 | } catch { 87 | fatalError("An error occurred: \(error.localizedDescription)") 88 | } 89 | } 90 | 91 | task.resume() 92 | } 93 | 94 | // MARK: Personalized Apple Music API Methods 95 | 96 | func performAppleMusicGetUserStorefront(userToken: String, completion: @escaping GetUserStorefrontCompletionHandler) { 97 | guard let developerToken = fetchDeveloperToken() else { 98 | fatalError("Developer Token not configured. See README for more details.") 99 | } 100 | 101 | let urlRequest = AppleMusicRequestFactory.createGetUserStorefrontRequest(developerToken: developerToken, userToken: userToken) 102 | 103 | let task = urlSession.dataTask(with: urlRequest) { [weak self] (data, response, error) in 104 | guard error == nil, let urlResponse = response as? HTTPURLResponse, urlResponse.statusCode == 200 else { 105 | let error = NSError(domain: "AppleMusicManagerErrorDomain", code: -9000, userInfo: [NSUnderlyingErrorKey: error!]) 106 | 107 | completion(nil, error) 108 | 109 | return 110 | } 111 | 112 | do { 113 | 114 | let identifier = try self?.processStorefront(from: data!) 115 | 116 | completion(identifier, nil) 117 | } catch { 118 | fatalError("An error occurred: \(error.localizedDescription)") 119 | } 120 | } 121 | 122 | task.resume() 123 | } 124 | 125 | func processMediaItemSections(from json: Data) throws -> [[MediaItem]] { 126 | guard let jsonDictionary = try JSONSerialization.jsonObject(with: json, options: []) as? [String: Any], 127 | let results = jsonDictionary[ResponseRootJSONKeys.results] as? [String: [String: Any]] else { 128 | throw SerializationError.missing(ResponseRootJSONKeys.results) 129 | } 130 | 131 | var mediaItems = [[MediaItem]]() 132 | 133 | if let songsDictionary = results[ResourceTypeJSONKeys.songs] { 134 | 135 | if let dataArray = songsDictionary[ResponseRootJSONKeys.data] as? [[String: Any]] { 136 | let songMediaItems = try processMediaItems(from: dataArray) 137 | mediaItems.append(songMediaItems) 138 | } 139 | } 140 | 141 | if let albumsDictionary = results[ResourceTypeJSONKeys.albums] { 142 | 143 | if let dataArray = albumsDictionary[ResponseRootJSONKeys.data] as? [[String: Any]] { 144 | let albumMediaItems = try processMediaItems(from: dataArray) 145 | mediaItems.append(albumMediaItems) 146 | } 147 | } 148 | 149 | return mediaItems 150 | } 151 | 152 | func processMediaItems(from json: [[String: Any]]) throws -> [MediaItem] { 153 | let songMediaItems = try json.map { try MediaItem(json: $0) } 154 | return songMediaItems 155 | } 156 | 157 | func processStorefront(from json: Data) throws -> String { 158 | guard let jsonDictionary = try JSONSerialization.jsonObject(with: json, options: []) as? [String: Any], 159 | let data = jsonDictionary[ResponseRootJSONKeys.data] as? [[String: Any]] else { 160 | throw SerializationError.missing(ResponseRootJSONKeys.data) 161 | } 162 | 163 | guard let identifier = data.first?[ResourceJSONKeys.identifier] as? String else { 164 | throw SerializationError.missing(ResourceJSONKeys.identifier) 165 | } 166 | 167 | return identifier 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /AppleMusicSample/Controllers/AuthorizationDataSource.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | `AuthorizationDataSource` is the data source for the `AuthorizationTableViewController` that provides the current authorization 6 | information for the application. 7 | */ 8 | 9 | import UIKit 10 | import StoreKit 11 | import MediaPlayer 12 | 13 | class AuthorizationDataSource { 14 | 15 | enum SectionTypes: Int { 16 | case mediaLibraryAuthorizationStatus = 0, cloudServiceAuthorizationStatus, requestCapabilities 17 | 18 | func sectionTitle() -> String { 19 | switch self { 20 | case .cloudServiceAuthorizationStatus: 21 | return "SKCloudServiceController" 22 | case .requestCapabilities: 23 | return "Capabilities" 24 | case .mediaLibraryAuthorizationStatus: 25 | return "MPMediaLibrary" 26 | } 27 | } 28 | } 29 | 30 | let authorizationManager: AuthorizationManager 31 | 32 | var capabilities = [SKCloudServiceCapability]() 33 | 34 | // MARK: Initialization 35 | 36 | init(authorizationManager: AuthorizationManager) { 37 | self.authorizationManager = authorizationManager 38 | } 39 | 40 | // MARK: Data Source Methods 41 | 42 | public func numberOfSections() -> Int { 43 | // There is always a section for the displaying +authorizationStatus from `SKCloudServiceController` and `MPMediaLibrary`. 44 | var section = 2 45 | 46 | // If we have capabilities to display from +requestCapabilities from SKCloudServiceController. 47 | if SKCloudServiceController.authorizationStatus() == .authorized { 48 | 49 | let cloudServiceCapabilities = authorizationManager.cloudServiceCapabilities 50 | 51 | capabilities = [] 52 | 53 | if cloudServiceCapabilities.contains(.addToCloudMusicLibrary) { 54 | capabilities.append(.addToCloudMusicLibrary) 55 | } 56 | 57 | if cloudServiceCapabilities.contains(.musicCatalogPlayback) { 58 | capabilities.append(.musicCatalogPlayback) 59 | } 60 | 61 | if cloudServiceCapabilities.contains(.musicCatalogSubscriptionEligible) { 62 | capabilities.append(.musicCatalogSubscriptionEligible) 63 | } 64 | 65 | section += 1 66 | } 67 | 68 | return section 69 | } 70 | 71 | public func numberOfItems(in section: Int) -> Int { 72 | guard let sectionType = SectionTypes(rawValue: section) else { 73 | return 0 74 | } 75 | 76 | switch sectionType { 77 | case .cloudServiceAuthorizationStatus: 78 | return 1 79 | case .requestCapabilities: 80 | return capabilities.count 81 | case .mediaLibraryAuthorizationStatus: 82 | return 1 83 | } 84 | } 85 | 86 | public func sectionTitle(for section: Int) -> String { 87 | guard let sectionType = SectionTypes(rawValue: section) else { 88 | return "" 89 | } 90 | 91 | return sectionType.sectionTitle() 92 | } 93 | 94 | public func stringForItem(at indexPath: IndexPath) -> String { 95 | guard let sectionType = SectionTypes(rawValue: indexPath.section) else { 96 | return "" 97 | } 98 | 99 | switch sectionType { 100 | case .cloudServiceAuthorizationStatus: 101 | return SKCloudServiceController.authorizationStatus().statusString() 102 | case .requestCapabilities: 103 | return capabilities[indexPath.row].capabilityString() 104 | case .mediaLibraryAuthorizationStatus: 105 | return MPMediaLibrary.authorizationStatus().statusString() 106 | } 107 | } 108 | } 109 | 110 | // MARK: Helpful Extension Methods 111 | 112 | extension SKCloudServiceAuthorizationStatus { 113 | func statusString() -> String { 114 | switch self { 115 | case .notDetermined: 116 | return "Not Determined" 117 | case .denied: 118 | return "Denied" 119 | case .restricted: 120 | return "Restricted" 121 | case .authorized: 122 | return "Authorized" 123 | } 124 | } 125 | } 126 | 127 | extension MPMediaLibraryAuthorizationStatus { 128 | func statusString() -> String { 129 | switch self { 130 | case .notDetermined: 131 | return "Not Determined" 132 | case .denied: 133 | return "Denied" 134 | case .restricted: 135 | return "Restricted" 136 | case .authorized: 137 | return "Authorized" 138 | } 139 | } 140 | } 141 | 142 | extension SKCloudServiceCapability { 143 | func capabilityString() -> String { 144 | switch self { 145 | case .addToCloudMusicLibrary: 146 | return "Add To Cloud Music Library" 147 | case .musicCatalogPlayback: 148 | return "Music Catalog Playback" 149 | case .musicCatalogSubscriptionEligible: 150 | return "Music Catalog Subscription Eligible" 151 | default: 152 | return "" 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /AppleMusicSample/Controllers/AuthorizationManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | `AuthorizationManager` manages requesting authorization from the user for modifying the user's `MPMediaLibrary` and querying 6 | the currently logged in iTunes Store account's Apple Music capabilities. 7 | */ 8 | 9 | import Foundation 10 | import StoreKit 11 | import MediaPlayer 12 | 13 | @objcMembers 14 | class AuthorizationManager: NSObject { 15 | 16 | // MARK: Types 17 | 18 | /// Notification that is posted whenever there is a change in the capabilities or Storefront identifier of the `SKCloudServiceController`. 19 | static let cloudServiceDidUpdateNotification = Notification.Name("cloudServiceDidUpdateNotification") 20 | 21 | /// Notification that is posted whenever there is a change in the authorization status that other parts of the sample should respond to. 22 | static let authorizationDidUpdateNotification = Notification.Name("authorizationDidUpdateNotification") 23 | 24 | /// The `UserDefaults` key for storing and retrieving the Music User Token associated with the currently signed in iTunes Store account. 25 | static let userTokenUserDefaultsKey = "UserTokenUserDefaultsKey" 26 | 27 | // MARK: Properties 28 | 29 | /// The instance of `SKCloudServiceController` that will be used for querying the available `SKCloudServiceCapability` and Storefront Identifier. 30 | let cloudServiceController = SKCloudServiceController() 31 | 32 | /// The instance of `AppleMusicManager` that will be used for querying storefront information and user token. 33 | let appleMusicManager: AppleMusicManager 34 | 35 | /// The current set of `SKCloudServiceCapability` that the sample can currently use. 36 | var cloudServiceCapabilities = SKCloudServiceCapability() 37 | 38 | /// The current set of two letter country code associated with the currently authenticated iTunes Store account. 39 | var cloudServiceStorefrontCountryCode = "" 40 | 41 | /// The Music User Token associated with the currently signed in iTunes Store account. 42 | var userToken = "" 43 | 44 | // MARK: Initialization 45 | 46 | init(appleMusicManager: AppleMusicManager) { 47 | self.appleMusicManager = appleMusicManager 48 | 49 | super.init() 50 | 51 | let notificationCenter = NotificationCenter.default 52 | 53 | /* 54 | It is important that your application listens to the `SKCloudServiceCapabilitiesDidChangeNotification` and 55 | `SKStorefrontCountryCodeDidChangeNotification` notifications so that your application can update its state and functionality 56 | when these values change if needed. 57 | */ 58 | 59 | notificationCenter.addObserver(self, 60 | selector: #selector(requestCloudServiceCapabilities), 61 | name: .SKCloudServiceCapabilitiesDidChange, 62 | object: nil) 63 | if #available(iOS 11.0, *) { 64 | notificationCenter.addObserver(self, 65 | selector: #selector(requestStorefrontCountryCode), 66 | name: .SKStorefrontCountryCodeDidChange, 67 | object: nil) 68 | } 69 | 70 | /* 71 | If the application has already been authorized in a previous run or manually by the user then it can request 72 | the current set of `SKCloudServiceCapability` and Storefront Identifier. 73 | */ 74 | if SKCloudServiceController.authorizationStatus() == .authorized { 75 | requestCloudServiceCapabilities() 76 | 77 | /// Retrieve the Music User Token for use in the application if it was stored from a previous run. 78 | if let token = UserDefaults.standard.string(forKey: AuthorizationManager.userTokenUserDefaultsKey) { 79 | userToken = token 80 | } else { 81 | /// The token was not stored previously then request one. 82 | requestUserToken() 83 | } 84 | } 85 | } 86 | 87 | deinit { 88 | // Remove all notification observers. 89 | let notificationCenter = NotificationCenter.default 90 | 91 | notificationCenter.removeObserver(self, name: .SKCloudServiceCapabilitiesDidChange, object: nil) 92 | 93 | if #available(iOS 11.0, *) { 94 | notificationCenter.removeObserver(self, name: .SKStorefrontCountryCodeDidChange, object: nil) 95 | } 96 | 97 | } 98 | 99 | // MARK: Authorization Request Methods 100 | 101 | func requestCloudServiceAuthorization() { 102 | /* 103 | An application should only ever call `SKCloudServiceController.requestAuthorization(_:)` when their 104 | current authorization is `SKCloudServiceAuthorizationStatusNotDetermined` 105 | */ 106 | guard SKCloudServiceController.authorizationStatus() == .notDetermined else { return } 107 | 108 | /* 109 | `SKCloudServiceController.requestAuthorization(_:)` triggers a prompt for the user asking if they wish to allow the application 110 | that requested authorization access to the device's cloud services information. This allows the application to query information 111 | such as the what capabilities the currently authenticated iTunes Store account has and if the account is eligible for an Apple Music 112 | Subscription Trial. 113 | 114 | This prompt will also include the value provided in the application's Info.plist for the `NSAppleMusicUsageDescription` key. 115 | This usage description should reflect what the application intends to use this access for. 116 | */ 117 | 118 | SKCloudServiceController.requestAuthorization { [weak self] (authorizationStatus) in 119 | switch authorizationStatus { 120 | case .authorized: 121 | self?.requestCloudServiceCapabilities() 122 | self?.requestUserToken() 123 | default: 124 | break 125 | } 126 | 127 | NotificationCenter.default.post(name: AuthorizationManager.authorizationDidUpdateNotification, object: nil) 128 | } 129 | } 130 | 131 | func requestMediaLibraryAuthorization() { 132 | /* 133 | An application should only ever call `MPMediaLibrary.requestAuthorization(_:)` when their 134 | current authorization is `MPMediaLibraryAuthorizationStatusNotDetermined` 135 | */ 136 | guard MPMediaLibrary.authorizationStatus() == .notDetermined else { return } 137 | 138 | /* 139 | `MPMediaLibrary.requestAuthorization(_:)` triggers a prompt for the user asking if they wish to allow the application 140 | that requested authorization access to the device's media library. 141 | 142 | This prompt will also include the value provided in the application's Info.plist for the `NSAppleMusicUsageDescription` key. 143 | This usage description should reflect what the application intends to use this access for. 144 | */ 145 | 146 | MPMediaLibrary.requestAuthorization { (_) in 147 | NotificationCenter.default.post(name: AuthorizationManager.cloudServiceDidUpdateNotification, object: nil) 148 | } 149 | } 150 | 151 | // MARK: `SKCloudServiceController` Related Methods 152 | 153 | func requestCloudServiceCapabilities() { 154 | cloudServiceController.requestCapabilities(completionHandler: { [weak self] (cloudServiceCapability, error) in 155 | guard error == nil else { 156 | fatalError("An error occurred when requesting capabilities: \(error!.localizedDescription)") 157 | } 158 | 159 | self?.cloudServiceCapabilities = cloudServiceCapability 160 | 161 | NotificationCenter.default.post(name: AuthorizationManager.cloudServiceDidUpdateNotification, object: nil) 162 | }) 163 | } 164 | 165 | func requestStorefrontCountryCode() { 166 | let completionHandler: (String?, Error?) -> Void = { [weak self] (countryCode, error) in 167 | guard error == nil else { 168 | print("An error occurred when requesting storefront country code: \(error!.localizedDescription)") 169 | return 170 | } 171 | 172 | guard let countryCode = countryCode else { 173 | print("Unexpected value from SKCloudServiceController for storefront country code.") 174 | return 175 | } 176 | 177 | self?.cloudServiceStorefrontCountryCode = countryCode 178 | 179 | NotificationCenter.default.post(name: AuthorizationManager.cloudServiceDidUpdateNotification, object: nil) 180 | } 181 | 182 | if SKCloudServiceController.authorizationStatus() == .authorized { 183 | if #available(iOS 11.0, *) { 184 | /* 185 | On iOS 11.0 or later, if the `SKCloudServiceController.authorizationStatus()` is `.authorized` then you can request the storefront 186 | country code. 187 | */ 188 | cloudServiceController.requestStorefrontCountryCode(completionHandler: completionHandler) 189 | } else { 190 | appleMusicManager.performAppleMusicGetUserStorefront(userToken: userToken, completion: completionHandler) 191 | } 192 | } else { 193 | determineRegionWithDeviceLocale(completion: completionHandler) 194 | } 195 | } 196 | 197 | func requestUserToken() { 198 | guard let developerToken = appleMusicManager.fetchDeveloperToken() else { 199 | return 200 | } 201 | 202 | if SKCloudServiceController.authorizationStatus() == .authorized { 203 | 204 | let completionHandler: (String?, Error?) -> Void = { [weak self] (token, error) in 205 | guard error == nil else { 206 | print("An error occurred when requesting user token: \(error!.localizedDescription)") 207 | return 208 | } 209 | 210 | guard let token = token else { 211 | print("Unexpected value from SKCloudServiceController for user token.") 212 | return 213 | } 214 | 215 | self?.userToken = token 216 | 217 | /// Store the Music User Token for future use in your application. 218 | let userDefaults = UserDefaults.standard 219 | 220 | userDefaults.set(token, forKey: AuthorizationManager.userTokenUserDefaultsKey) 221 | userDefaults.synchronize() 222 | 223 | if self?.cloudServiceStorefrontCountryCode == "" { 224 | self?.requestStorefrontCountryCode() 225 | } 226 | 227 | NotificationCenter.default.post(name: AuthorizationManager.cloudServiceDidUpdateNotification, object: nil) 228 | } 229 | 230 | if #available(iOS 11.0, *) { 231 | cloudServiceController.requestUserToken(forDeveloperToken: developerToken, completionHandler: completionHandler) 232 | } else { 233 | cloudServiceController.requestPersonalizationToken(forClientToken: developerToken, withCompletionHandler: completionHandler) 234 | } 235 | } 236 | } 237 | 238 | func determineRegionWithDeviceLocale(completion: @escaping (String?, Error?) -> Void) { 239 | /* 240 | On other versions of iOS or when `SKCloudServiceController.authorizationStatus()` is not `.authorized`, your application should use a 241 | combination of the device's `Locale.current.regionCode` and the Apple Music API to make an approximation of the storefront to use. 242 | */ 243 | 244 | let currentRegionCode = Locale.current.regionCode?.lowercased() ?? "us" 245 | 246 | appleMusicManager.performAppleMusicStorefrontsLookup(regionCode: currentRegionCode, completion: completion) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /AppleMusicSample/Controllers/MediaLibraryManager.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | The `MediaLibraryManager` manages creating and updating the `MPMediaPlaylist` that the application creates. 6 | It also serves as the data source of the `PlaylistTableViewController` 7 | */ 8 | 9 | import Foundation 10 | import MediaPlayer 11 | 12 | @objcMembers 13 | class MediaLibraryManager: NSObject { 14 | 15 | // MARK: Types 16 | 17 | /// The Key for the `UserDefaults` value representing the UUID of the Playlist this sample creates. 18 | static let playlistUUIDKey = "playlistUUIDKey" 19 | 20 | /// Notification that is posted whenever the contents of the device's Media Library changed. 21 | static let libraryDidUpdate = Notification.Name("libraryDidUpdate") 22 | 23 | // MARK: Properties 24 | 25 | /// The instance of `AuthorizationManager` used for looking up the current device's Media Library and Cloud Services authorization status. 26 | let authorizationManager: AuthorizationManager 27 | 28 | /// The instance of `MPMediaPlaylist` that corresponds to the playlist created by this sample in the current device's Media Library. 29 | var mediaPlaylist: MPMediaPlaylist! 30 | 31 | // MARK: Initialization 32 | 33 | init(authorizationManager: AuthorizationManager) { 34 | self.authorizationManager = authorizationManager 35 | 36 | super.init() 37 | 38 | // Add the notification observers needed to respond to events from the `AuthorizationManager`, `MPMediaLibrary` and `UIApplication`. 39 | let notificationCenter = NotificationCenter.default 40 | 41 | notificationCenter.addObserver(self, 42 | selector: #selector(handleAuthorizationManagerAuthorizationDidUpdateNotification), 43 | name: AuthorizationManager.authorizationDidUpdateNotification, 44 | object: nil) 45 | 46 | notificationCenter.addObserver(self, 47 | selector: #selector(handleMediaLibraryDidChangeNotification), 48 | name: .MPMediaLibraryDidChange, 49 | object: nil) 50 | 51 | notificationCenter.addObserver(self, 52 | selector: #selector(handleMediaLibraryDidChangeNotification), 53 | name: .UIApplicationWillEnterForeground, 54 | object: nil) 55 | 56 | handleAuthorizationManagerAuthorizationDidUpdateNotification() 57 | } 58 | 59 | deinit { 60 | // Remove all notification observers. 61 | let notificationCenter = NotificationCenter.default 62 | 63 | notificationCenter.removeObserver(self, name: AuthorizationManager.authorizationDidUpdateNotification, object: nil) 64 | notificationCenter.removeObserver(self, name: NSNotification.Name.MPMediaLibraryDidChange, object: nil) 65 | notificationCenter.removeObserver(self, name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil) 66 | } 67 | 68 | func createPlaylistIfNeeded() { 69 | 70 | guard mediaPlaylist == nil else { return } 71 | 72 | // To create a new playlist or lookup a playlist there are several steps you need to do. 73 | let playlistUUID: UUID 74 | 75 | var playlistCreationMetadata: MPMediaPlaylistCreationMetadata! 76 | 77 | let userDefaults = UserDefaults.standard 78 | 79 | if let playlistUUIDString = userDefaults.string(forKey: MediaLibraryManager.playlistUUIDKey) { 80 | // In this case, the sample already created a playlist in a previous run. In this case we lookup the UUID that was used before. 81 | 82 | guard let uuid = UUID(uuidString: playlistUUIDString) else { 83 | fatalError("Failed to create UUID from existing UUID string: \(playlistUUIDString)") 84 | } 85 | 86 | playlistUUID = uuid 87 | } else { 88 | // Create an instance of `UUID` to identify the new playlist. 89 | playlistUUID = UUID() 90 | 91 | // Create an instance of `MPMediaPlaylistCreationMetadata`, this represents the metadata to associate with the new playlist. 92 | playlistCreationMetadata = MPMediaPlaylistCreationMetadata(name: "Hum Playlist") 93 | 94 | playlistCreationMetadata.descriptionText = "This playlist was created using \(Bundle.main.infoDictionary!["CFBundleName"]!) to demonstrate how to use the Apple Music APIs" 95 | 96 | // Store the `UUID` that the sample will use for looking up the playlist in the future. 97 | userDefaults.setValue(playlistUUID.uuidString, forKey: MediaLibraryManager.playlistUUIDKey) 98 | userDefaults.synchronize() 99 | } 100 | 101 | // Request the new or existing playlist from the device. 102 | MPMediaLibrary.default().getPlaylist(with: playlistUUID, creationMetadata: playlistCreationMetadata) { (playlist, error) in 103 | guard error == nil else { 104 | fatalError("An error occurred while retrieving/creating playlist: \(error!.localizedDescription)") 105 | } 106 | 107 | self.mediaPlaylist = playlist 108 | self.addItem(with: "203709340") 109 | NotificationCenter.default.post(name: MediaLibraryManager.libraryDidUpdate, object: nil) 110 | } 111 | } 112 | 113 | // MARK: Playlist Modification Method 114 | 115 | func addItem(with identifier: String) { 116 | 117 | guard let mediaPlaylist = mediaPlaylist else { 118 | fatalError("Playlist has not been created") 119 | } 120 | 121 | mediaPlaylist.addItem(withProductID: identifier, completionHandler: { (error) in 122 | guard error == nil else { 123 | fatalError("An error occurred while adding an item to the playlist: \(error!.localizedDescription)") 124 | } 125 | 126 | NotificationCenter.default.post(name: MediaLibraryManager.libraryDidUpdate, object: nil) 127 | }) 128 | } 129 | 130 | // MARK: Notification Observing Methods 131 | 132 | func handleAuthorizationManagerAuthorizationDidUpdateNotification() { 133 | 134 | if MPMediaLibrary.authorizationStatus() == .authorized { 135 | createPlaylistIfNeeded() 136 | } 137 | } 138 | 139 | func handleMediaLibraryDidChangeNotification() { 140 | 141 | if MPMediaLibrary.authorizationStatus() == .authorized { 142 | createPlaylistIfNeeded() 143 | } 144 | 145 | NotificationCenter.default.post(name: MediaLibraryManager.libraryDidUpdate, object: nil) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /AppleMusicSample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSApplicationCategoryType 22 | 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | NSAppleMusicUsageDescription 31 | Demonstrating using StoreKit and MediaPlayer APIs. 32 | UILaunchStoryboardName 33 | LaunchScreen 34 | UIMainStoryboardFile 35 | Main 36 | UIRequiredDeviceCapabilities 37 | 38 | armv7 39 | 40 | UISupportedInterfaceOrientations 41 | 42 | UIInterfaceOrientationPortrait 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /AppleMusicSample/Model/Artwork.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | `Artwork` represents a `Artwork` object from the Apple Music Web Services. 6 | */ 7 | 8 | import UIKit 9 | 10 | class Artwork { 11 | 12 | // MARK: Types 13 | 14 | /// The various keys needed for serializing an instance of `Artwork` using a JSON response from the Apple Music Web Service. 15 | struct JSONKeys { 16 | static let height = "height" 17 | 18 | static let width = "width" 19 | 20 | static let url = "url" 21 | } 22 | 23 | // MARK: Properties 24 | 25 | /// The maximum height available for the image. 26 | let height: Int 27 | 28 | /// The maximum width available for the image. 29 | let width: Int 30 | 31 | /** 32 | The string representation of the URL to request the image asset. This template should be used to create the URL for the correctly sized image 33 | your application wishes to use. See `Artwork.imageURL(size:)` for additional information. 34 | */ 35 | let urlTemplateString: String 36 | 37 | // MARK: Initialization 38 | 39 | init(json: [String: Any]) throws { 40 | guard let height = json[JSONKeys.height] as? Int else { 41 | throw SerializationError.missing(JSONKeys.height) 42 | } 43 | 44 | guard let width = json[JSONKeys.width] as? Int else { 45 | throw SerializationError.missing(JSONKeys.width) 46 | } 47 | 48 | guard let urlTemplateString = json[JSONKeys.url] as? String else { 49 | throw SerializationError.missing(JSONKeys.url) 50 | } 51 | 52 | self.height = height 53 | self.width = width 54 | self.urlTemplateString = urlTemplateString 55 | } 56 | 57 | // MARK: Image URL Generation Method 58 | 59 | func imageURL(size: CGSize) -> URL { 60 | 61 | /* 62 | There are three pieces of information needed to create the URL for the image we want for a given size. This information is the width, height 63 | and image format. We can use this information in addition to the `urlTemplateString` to create the URL for the image we wish to use. 64 | */ 65 | 66 | // 1) Replace the "{w}" placeholder with the desired width as an integer value. 67 | var imageURLString = urlTemplateString.replacingOccurrences(of: "{w}", with: "\(Int(size.width))") 68 | 69 | // 2) Replace the "{h}" placeholder with the desired height as an integer value. 70 | imageURLString = imageURLString.replacingOccurrences(of: "{h}", with: "\(Int(size.width))") 71 | 72 | // 3) Replace the "{f}" placeholder with the desired image format. 73 | imageURLString = imageURLString.replacingOccurrences(of: "{f}", with: "png") 74 | 75 | return URL(string: imageURLString)! 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /AppleMusicSample/Model/MediaItem.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | `MediaItem` represents a `Resource` object from the Apple Music Web Services. 6 | */ 7 | 8 | import Foundation 9 | 10 | class MediaItem { 11 | 12 | // MARK: Types 13 | 14 | /// The type of resource. 15 | /// 16 | /// - songs: This indicates that the `MediaItem` is a song from the Apple Music Catalog. 17 | /// - albums: This indicates that the `MediaItem` is an album from the Apple Music Catalog. 18 | enum MediaType: String { 19 | case songs, albums, stations, playlists 20 | } 21 | 22 | /// The various keys needed for serializing an instance of `MediaItem` using a JSON response from the Apple Music Web Service. 23 | struct JSONKeys { 24 | static let identifier = "id" 25 | 26 | static let type = "type" 27 | 28 | static let attributes = "attributes" 29 | 30 | static let name = "name" 31 | 32 | static let artistName = "artistName" 33 | 34 | static let artwork = "artwork" 35 | } 36 | 37 | // MARK: Properties 38 | 39 | /// The persistent identifier of the resource which is used to add the item to the playlist or trigger playback. 40 | let identifier: String 41 | 42 | /// The localized name of the album or song. 43 | let name: String 44 | 45 | /// The artist’s name. 46 | let artistName: String 47 | 48 | /// The album artwork associated with the song or album. 49 | let artwork: Artwork 50 | 51 | /// The type of the `MediaItem` which in this application can be either `songs` or `albums`. 52 | let type: MediaType 53 | 54 | // MARK: Initialization 55 | 56 | init(json: [String: Any]) throws { 57 | guard let identifier = json[JSONKeys.identifier] as? String else { 58 | throw SerializationError.missing(JSONKeys.identifier) 59 | } 60 | 61 | guard let typeString = json[JSONKeys.type] as? String, let type = MediaType(rawValue: typeString) else { 62 | throw SerializationError.missing(JSONKeys.type) 63 | } 64 | 65 | guard let attributes = json[JSONKeys.attributes] as? [String: Any] else { 66 | throw SerializationError.missing(JSONKeys.attributes) 67 | } 68 | 69 | guard let name = attributes[JSONKeys.name] as? String else { 70 | throw SerializationError.missing(JSONKeys.name) 71 | } 72 | 73 | let artistName = attributes[JSONKeys.artistName] as? String ?? " " 74 | 75 | guard let artworkJSON = attributes[JSONKeys.artwork] as? [String: Any], let artwork = try? Artwork(json: artworkJSON) else { 76 | throw SerializationError.missing(JSONKeys.artwork) 77 | } 78 | 79 | self.identifier = identifier 80 | self.type = type 81 | self.name = name 82 | self.artistName = artistName 83 | self.artwork = artwork 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /AppleMusicSample/Model/SerializationError.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | `SerializationError` is an `Error` enum that represents a JSON serialization error. 6 | */ 7 | 8 | import Foundation 9 | 10 | enum SerializationError: Error { 11 | 12 | /// This case indicates that the expected field in the JSON object is not found. 13 | case missing(String) 14 | } 15 | -------------------------------------------------------------------------------- /AppleMusicSample/Resources/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 | } -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Assets.imageset/Assets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HumApp/MusicKit/5a624738e674da6cb816723a69e0022e20d3256a/AppleMusicSample/Resources/Assets.xcassets/Assets.imageset/Assets.png -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Assets.imageset/Assets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HumApp/MusicKit/5a624738e674da6cb816723a69e0022e20d3256a/AppleMusicSample/Resources/Assets.xcassets/Assets.imageset/Assets@2x.png -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Assets.imageset/Assets@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HumApp/MusicKit/5a624738e674da6cb816723a69e0022e20d3256a/AppleMusicSample/Resources/Assets.xcassets/Assets.imageset/Assets@3x.png -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Assets.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Assets.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Assets@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Assets@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Backward.imageset/Backward.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HumApp/MusicKit/5a624738e674da6cb816723a69e0022e20d3256a/AppleMusicSample/Resources/Assets.xcassets/Backward.imageset/Backward.pdf -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Backward.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Backward.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Configure.imageset/Configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HumApp/MusicKit/5a624738e674da6cb816723a69e0022e20d3256a/AppleMusicSample/Resources/Assets.xcassets/Configure.imageset/Configure.png -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Configure.imageset/Configure@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HumApp/MusicKit/5a624738e674da6cb816723a69e0022e20d3256a/AppleMusicSample/Resources/Assets.xcassets/Configure.imageset/Configure@2x.png -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Configure.imageset/Configure@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HumApp/MusicKit/5a624738e674da6cb816723a69e0022e20d3256a/AppleMusicSample/Resources/Assets.xcassets/Configure.imageset/Configure@3x.png -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Configure.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Configure.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "Configure@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "Configure@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Forward.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Forward.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Forward.imageset/Forward.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HumApp/MusicKit/5a624738e674da6cb816723a69e0022e20d3256a/AppleMusicSample/Resources/Assets.xcassets/Forward.imageset/Forward.pdf -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Pause.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Pause.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Pause.imageset/Pause.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HumApp/MusicKit/5a624738e674da6cb816723a69e0022e20d3256a/AppleMusicSample/Resources/Assets.xcassets/Pause.imageset/Pause.pdf -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Play.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "Play.pdf" 6 | } 7 | ], 8 | "info" : { 9 | "version" : 1, 10 | "author" : "xcode" 11 | } 12 | } -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Assets.xcassets/Play.imageset/Play.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HumApp/MusicKit/5a624738e674da6cb816723a69e0022e20d3256a/AppleMusicSample/Resources/Assets.xcassets/Play.imageset/Play.pdf -------------------------------------------------------------------------------- /AppleMusicSample/Resources/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 | 27 | 28 | -------------------------------------------------------------------------------- /AppleMusicSample/Resources/Base.lproj/Main.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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 67 | 73 | 74 | 75 | 76 | 83 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | -------------------------------------------------------------------------------- /AppleMusicSample/Utility/AppleMusicRequestFactory.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | The `AppleMusicRequestFactory` type is used to build the various Apple Music API calls used by the sample. 6 | */ 7 | 8 | import Foundation 9 | 10 | struct AppleMusicRequestFactory { 11 | 12 | // MARK: Types 13 | 14 | /// The base URL for all Apple Music API network calls. 15 | static let appleMusicAPIBaseURLString = "api.music.apple.com" 16 | 17 | /// The Apple Music API endpoint for requesting a list of recently played items. 18 | static let recentlyPlayedPathURLString = "/v1/me/recent/played" 19 | 20 | /// The Apple Music API endpoint for requesting a the storefront of the currently logged in iTunes Store account. 21 | static let userStorefrontPathURLString = "/v1/me/storefront" 22 | 23 | static func createSearchRequest(with term: String, countryCode: String, developerToken: String) -> URLRequest { 24 | 25 | // Create the URL components for the network call. 26 | var urlComponents = URLComponents() 27 | urlComponents.scheme = "https" 28 | urlComponents.host = AppleMusicRequestFactory.appleMusicAPIBaseURLString 29 | urlComponents.path = "/v1/catalog/\(countryCode)/search" 30 | 31 | let expectedTerms = term.replacingOccurrences(of: " ", with: "+") 32 | let urlParameters = ["term": expectedTerms, 33 | "limit": "10", 34 | "types": "songs,albums"] 35 | 36 | var queryItems = [URLQueryItem]() 37 | for (key, value) in urlParameters { 38 | queryItems.append(URLQueryItem(name: key, value: value)) 39 | } 40 | 41 | urlComponents.queryItems = queryItems 42 | 43 | // Create and configure the `URLRequest`. 44 | 45 | var urlRequest = URLRequest(url: urlComponents.url!) 46 | urlRequest.httpMethod = "GET" 47 | 48 | urlRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization") 49 | 50 | return urlRequest 51 | } 52 | 53 | static func createStorefrontsRequest(regionCode: String, developerToken: String) -> URLRequest { 54 | var urlComponents = URLComponents() 55 | urlComponents.scheme = "https" 56 | urlComponents.host = AppleMusicRequestFactory.appleMusicAPIBaseURLString 57 | urlComponents.path = "/v1/storefronts/\(regionCode)" 58 | 59 | // Create and configure the `URLRequest`. 60 | 61 | var urlRequest = URLRequest(url: urlComponents.url!) 62 | urlRequest.httpMethod = "GET" 63 | 64 | urlRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization") 65 | 66 | return urlRequest 67 | } 68 | 69 | static func createRecentlyPlayedRequest(developerToken: String, userToken: String) -> URLRequest { 70 | var urlComponents = URLComponents() 71 | urlComponents.scheme = "https" 72 | urlComponents.host = AppleMusicRequestFactory.appleMusicAPIBaseURLString 73 | urlComponents.path = AppleMusicRequestFactory.recentlyPlayedPathURLString 74 | 75 | // Create and configure the `URLRequest`. 76 | 77 | var urlRequest = URLRequest(url: urlComponents.url!) 78 | urlRequest.httpMethod = "GET" 79 | 80 | urlRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization") 81 | urlRequest.addValue(userToken, forHTTPHeaderField: "Music-User-Token") 82 | 83 | return urlRequest 84 | } 85 | 86 | static func createGetUserStorefrontRequest(developerToken: String, userToken: String) -> URLRequest { 87 | var urlComponents = URLComponents() 88 | urlComponents.scheme = "https" 89 | urlComponents.host = AppleMusicRequestFactory.appleMusicAPIBaseURLString 90 | urlComponents.path = AppleMusicRequestFactory.userStorefrontPathURLString 91 | 92 | // Create and configure the `URLRequest`. 93 | 94 | var urlRequest = URLRequest(url: urlComponents.url!) 95 | urlRequest.httpMethod = "GET" 96 | 97 | urlRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization") 98 | urlRequest.addValue(userToken, forHTTPHeaderField: "Music-User-Token") 99 | 100 | return urlRequest 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /AppleMusicSample/Utility/JSONKeys.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | Various JSON keys needed when making calls to the Apple Music API. 6 | */ 7 | 8 | import Foundation 9 | 10 | /// Keys related to the `Response Root` JSON object in the Apple Music API. 11 | struct ResponseRootJSONKeys { 12 | static let data = "data" 13 | 14 | static let results = "results" 15 | } 16 | 17 | /// Keys related to the `Resource` JSON object in the Apple Music API. 18 | struct ResourceJSONKeys { 19 | static let identifier = "id" 20 | 21 | static let attributes = "attributes" 22 | 23 | static let type = "type" 24 | } 25 | 26 | /// The various keys needed for parsing a JSON response from the Apple Music Web Service. 27 | struct ResourceTypeJSONKeys { 28 | static let songs = "songs" 29 | 30 | static let albums = "albums" 31 | } 32 | -------------------------------------------------------------------------------- /AppleMusicSample/Views/AuthorizationTableViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | `AuthorizationViewController` is a `UIViewController` subclass that displays the current authorization status of the application. 6 | It also provides a way to request authorization if needed as well as preesnts the `SKCloudServiceSetupViewController` if appropriate. 7 | */ 8 | 9 | import UIKit 10 | import StoreKit 11 | import MediaPlayer 12 | 13 | @objcMembers 14 | class AuthorizationTableViewController: UITableViewController { 15 | 16 | // MARK: Properties 17 | 18 | /// The instance of `AuthorizationManager` used for querying and requesting authorization status. 19 | var authorizationManager: AuthorizationManager! 20 | 21 | /// The instance of `AuthorizationDataSource` that provides information for the `UITableView`. 22 | var authorizationDataSource: AuthorizationDataSource! 23 | 24 | /// A boolean value representing if a `SKCloudServiceSetupViewController` was presented while the application was running. 25 | var didPresentCloudServiceSetup = false 26 | 27 | /// View Life Cycle Methods. 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | 32 | authorizationDataSource = AuthorizationDataSource(authorizationManager: authorizationManager) 33 | 34 | // Add the notification observers needed to respond to events from the `AuthorizationManager` and `UIApplication`. 35 | let notificationCenter = NotificationCenter.default 36 | 37 | notificationCenter.addObserver(self, 38 | selector: #selector(handleAuthorizationManagerDidUpdateNotification), 39 | name: AuthorizationManager.cloudServiceDidUpdateNotification, 40 | object: nil) 41 | 42 | notificationCenter.addObserver(self, 43 | selector: #selector(handleAuthorizationManagerDidUpdateNotification), 44 | name: AuthorizationManager.authorizationDidUpdateNotification, 45 | object: nil) 46 | 47 | notificationCenter.addObserver(self, 48 | selector: #selector(handleAuthorizationManagerDidUpdateNotification), 49 | name: .UIApplicationWillEnterForeground, 50 | object: nil) 51 | 52 | setAuthorizationRequestButtonState() 53 | } 54 | 55 | override func viewWillAppear(_ animated: Bool) { 56 | super.viewWillAppear(animated) 57 | 58 | setAuthorizationRequestButtonState() 59 | } 60 | 61 | deinit { 62 | // Remove all notification observers. 63 | let notificationCenter = NotificationCenter.default 64 | 65 | notificationCenter.removeObserver(self, 66 | name: AuthorizationManager.cloudServiceDidUpdateNotification, 67 | object: nil) 68 | notificationCenter.removeObserver(self, 69 | name: AuthorizationManager.authorizationDidUpdateNotification, 70 | object: nil) 71 | notificationCenter.removeObserver(self, 72 | name: .UIApplicationWillEnterForeground, 73 | object: nil) 74 | } 75 | 76 | // MARK: UI Updating Methods 77 | 78 | func setAuthorizationRequestButtonState() { 79 | if SKCloudServiceController.authorizationStatus() == .notDetermined || MPMediaLibrary.authorizationStatus() == .notDetermined { 80 | self.navigationItem.rightBarButtonItem?.isEnabled = true 81 | } else { 82 | self.navigationItem.rightBarButtonItem?.isEnabled = false 83 | } 84 | } 85 | 86 | // MARK: Target-Action Methods 87 | 88 | @IBAction func requestAuthorization(_ sender: UIBarButtonItem) { 89 | authorizationManager.requestCloudServiceAuthorization() 90 | 91 | authorizationManager.requestMediaLibraryAuthorization() 92 | } 93 | 94 | // MARK: - Table view data source 95 | 96 | override func numberOfSections(in tableView: UITableView) -> Int { 97 | return authorizationDataSource.numberOfSections() 98 | } 99 | 100 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 101 | return authorizationDataSource.numberOfItems(in: section) 102 | } 103 | 104 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 105 | return authorizationDataSource.sectionTitle(for: section) 106 | } 107 | 108 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 109 | let cell = tableView.dequeueReusableCell(withIdentifier: "AuthorizationCellIdentifier", for: indexPath) 110 | 111 | cell.textLabel?.text = authorizationDataSource.stringForItem(at: indexPath) 112 | 113 | return cell 114 | } 115 | 116 | // MARK: SKCloudServiceSetupViewController Method 117 | 118 | func presentCloudServiceSetup() { 119 | 120 | guard didPresentCloudServiceSetup == false else { 121 | return 122 | } 123 | 124 | /* 125 | If the current `SKCloudServiceCapability` includes `.musicCatalogSubscriptionEligible`, this means that the currently signed in iTunes Store 126 | account is elgible for an Apple Music Trial Subscription. To provide the user with an option to sign up for a free trial, your application 127 | can present the `SKCloudServiceSetupViewController` as demonstrated below. 128 | */ 129 | 130 | let cloudServiceSetupViewController = SKCloudServiceSetupViewController() 131 | cloudServiceSetupViewController.delegate = self 132 | 133 | cloudServiceSetupViewController.load(options: [.action: SKCloudServiceSetupAction.subscribe]) { [weak self] (result, error) in 134 | guard error == nil else { 135 | fatalError("An Error occurred: \(error!.localizedDescription)") 136 | } 137 | 138 | if result { 139 | self?.present(cloudServiceSetupViewController, animated: true, completion: nil) 140 | self?.didPresentCloudServiceSetup = true 141 | } 142 | } 143 | } 144 | 145 | // MARK: Notification Observing Methods 146 | 147 | func handleAuthorizationManagerDidUpdateNotification() { 148 | DispatchQueue.main.async { 149 | if SKCloudServiceController.authorizationStatus() == .notDetermined || MPMediaLibrary.authorizationStatus() == .notDetermined { 150 | self.navigationItem.rightBarButtonItem?.isEnabled = true 151 | } else { 152 | self.navigationItem.rightBarButtonItem?.isEnabled = false 153 | 154 | if self.authorizationManager.cloudServiceCapabilities.contains(.musicCatalogSubscriptionEligible) && 155 | !self.authorizationManager.cloudServiceCapabilities.contains(.musicCatalogPlayback) { 156 | self.presentCloudServiceSetup() 157 | } 158 | 159 | } 160 | 161 | DispatchQueue.main.async { 162 | self.tableView.reloadData() 163 | } 164 | } 165 | } 166 | } 167 | 168 | extension AuthorizationTableViewController: SKCloudServiceSetupViewControllerDelegate { 169 | func cloudServiceSetupViewControllerDidDismiss(_ cloudServiceSetupViewController: SKCloudServiceSetupViewController) { 170 | DispatchQueue.main.async { 171 | self.tableView.reloadData() 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /AppleMusicSample/Views/MediaItemTableViewCell.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | MediaSearchTableViewCell` is a `UITableViewCell` subclass that represents an `MediaItem` in the search results from the 6 | Apple Music Catalog in the `MediaSearchTableViewController`. 7 | */ 8 | 9 | import UIKit 10 | 11 | class MediaItemTableViewCell: UITableViewCell { 12 | 13 | // MARK: Types 14 | 15 | static let identifier = "MediaItemTableViewCell" 16 | 17 | // MARK: Properties 18 | 19 | /// The `UIImageView` for displaying the artwork of the currently playing `MediaItem`. 20 | @IBOutlet weak var assetCoverArtImageView: UIImageView! 21 | 22 | /// The 'UILabel` for displaying the title of `MediaItem`. 23 | @IBOutlet weak var mediaItemTitleLabel: UILabel! 24 | 25 | /// The 'UILabel` for displaying the artist of `MediaItem`. 26 | @IBOutlet weak var mediaItemArtistLabel: UILabel! 27 | 28 | /// The 'UIButton` for adding the `MediaItem` to the application's `MPMediaPlaylist`. 29 | @IBOutlet weak var addToPlaylistButton: UIButton! 30 | 31 | /// The 'UIButton` for playing the `MediaItem`. 32 | @IBOutlet weak var playItemButton: UIButton! 33 | 34 | /// The `MediaSearchTableViewCellDelegate` that will respond to user interaction events from the `MediaSearchTableViewCell`. 35 | weak var delegate: MediaSearchTableViewCellDelegate? 36 | 37 | var mediaItem: MediaItem? { 38 | didSet { 39 | mediaItemTitleLabel.text = mediaItem?.name ?? "" 40 | mediaItemArtistLabel.text = mediaItem?.artistName ?? "" 41 | assetCoverArtImageView.image = nil 42 | } 43 | } 44 | 45 | // MARK: Target-Action Methods 46 | 47 | @IBAction func addToPlaylist(_ sender: UIButton) { 48 | if let mediaItem = mediaItem { 49 | delegate?.mediaSearchTableViewCell(self, addToPlaylist: mediaItem) 50 | } 51 | } 52 | 53 | } 54 | 55 | protocol MediaSearchTableViewCellDelegate: class { 56 | func mediaSearchTableViewCell(_ mediaSearchTableViewCell: MediaItemTableViewCell, addToPlaylist mediaItem: MediaItem) 57 | 58 | } 59 | -------------------------------------------------------------------------------- /AppleMusicSample/Views/MediaSearchTableViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | See LICENSE folder for this sample’s licensing information. 3 | 4 | Abstract: 5 | `MediaSearchTableViewController` is a `UITableViewController` subclass that allows performing a search on the Apple Music Catalog 6 | and displays the search results. The results can then either be played using the `MusicPlayerManager` or added to the 7 | `MPMediaPlaylist` created by the `MediaLibraryManager`. 8 | */ 9 | 10 | import UIKit 11 | import StoreKit 12 | 13 | @objcMembers 14 | class MediaSearchTableViewController: UITableViewController { 15 | 16 | /// The instance of `UISearchController` used for providing the search funcationality in the `UITableView`. 17 | var searchController = UISearchController(searchResultsController: nil) 18 | 19 | /// The instance of `AuthorizationManager` used for querying and requesting authorization status. 20 | var authorizationManager: AuthorizationManager! 21 | 22 | /// The instance of `AppleMusicManager` which is used to make search request calls to the Apple Music Web Services. 23 | let appleMusicManager = AppleMusicManager() 24 | 25 | /// The instance of `MediaLibraryManager` which is used for adding items to the application's playlist. 26 | var mediaLibraryManager: MediaLibraryManager! 27 | 28 | /// A `DispatchQueue` used for synchornizing the setting of `mediaItems` to avoid threading issues with various `UITableView` delegate callbacks. 29 | var setterQueue = DispatchQueue(label: "MediaSearchTableViewController") 30 | 31 | /// The array of `MediaItem` objects that represents the list of search results. 32 | var mediaItems = [[MediaItem]]() { 33 | didSet { 34 | DispatchQueue.main.async { 35 | self.tableView.reloadData() 36 | } 37 | } 38 | } 39 | 40 | // MARK: View Life Cycle Methods 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | 45 | // Configure self sizing cells. 46 | tableView.rowHeight = UITableViewAutomaticDimension 47 | tableView.estimatedRowHeight = 100 48 | 49 | // Configure the `UISearchController`. 50 | searchController.searchResultsUpdater = self 51 | searchController.dimsBackgroundDuringPresentation = false 52 | definesPresentationContext = true 53 | searchController.searchBar.delegate = self 54 | tableView.tableHeaderView = searchController.searchBar 55 | 56 | /* 57 | Add the notification observers needed to respond to events from the `AuthorizationManager`, `MPMediaLibrary` and `UIApplication`. 58 | This is so that if the user enables/disables capabilities in the Settings app the application will reflect those changes accurately. 59 | */ 60 | let notificationCenter = NotificationCenter.default 61 | 62 | notificationCenter.addObserver(self, 63 | selector: #selector(handleAuthorizationManagerAuthorizationDidUpdateNotification), 64 | name: AuthorizationManager.authorizationDidUpdateNotification, 65 | object: nil) 66 | 67 | notificationCenter.addObserver(self, 68 | selector: #selector(handleAuthorizationManagerAuthorizationDidUpdateNotification), 69 | name: .UIApplicationWillEnterForeground, 70 | object: nil) 71 | } 72 | 73 | deinit { 74 | // Remove all notification observers. 75 | let notificationCenter = NotificationCenter.default 76 | 77 | notificationCenter.removeObserver(self, name: AuthorizationManager.authorizationDidUpdateNotification, object: nil) 78 | notificationCenter.removeObserver(self, name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil) 79 | } 80 | 81 | override func viewWillAppear(_ animated: Bool) { 82 | super.viewWillAppear(animated) 83 | 84 | if appleMusicManager.fetchDeveloperToken() == nil { 85 | 86 | searchController.searchBar.isUserInteractionEnabled = false 87 | 88 | let alertController = UIAlertController(title: "Error", 89 | message: "No developer token was specified. See the README for more information.", 90 | preferredStyle: .alert) 91 | alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.cancel, handler: nil)) 92 | present(alertController, animated: true, completion: nil) 93 | } else { 94 | searchController.searchBar.isUserInteractionEnabled = true 95 | } 96 | } 97 | 98 | // MARK: - Table view data source 99 | 100 | override func numberOfSections(in tableView: UITableView) -> Int { 101 | return mediaItems.count 102 | } 103 | 104 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 105 | return mediaItems[section].count 106 | } 107 | 108 | override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 109 | if section == 0 { 110 | return NSLocalizedString("Songs", comment: "Songs") 111 | } else { 112 | return NSLocalizedString("Albums", comment: "Albums") 113 | } 114 | } 115 | 116 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 117 | guard let cell = tableView.dequeueReusableCell(withIdentifier: MediaItemTableViewCell.identifier, 118 | for: indexPath) as? MediaItemTableViewCell else { 119 | return UITableViewCell() 120 | } 121 | 122 | let mediaItem = mediaItems[indexPath.section][indexPath.row] 123 | 124 | cell.mediaItem = mediaItem 125 | cell.delegate = self 126 | 127 | let cloudServiceCapabilities = authorizationManager.cloudServiceCapabilities 128 | 129 | /* 130 | It is important to actually check if your application has the appropriate `SKCloudServiceCapability` options before enabling functionality 131 | related to playing back content from the Apple Music Catalog or adding items to the user's Cloud Music Library. 132 | */ 133 | 134 | if cloudServiceCapabilities.contains(.addToCloudMusicLibrary) { 135 | cell.addToPlaylistButton.isEnabled = true 136 | } else { 137 | cell.addToPlaylistButton.isEnabled = false 138 | } 139 | 140 | if cloudServiceCapabilities.contains(.musicCatalogPlayback) { 141 | cell.playItemButton.isEnabled = true 142 | } else { 143 | cell.playItemButton.isEnabled = false 144 | } 145 | 146 | return cell 147 | } 148 | 149 | // MARK: Notification Observing Methods 150 | 151 | func handleAuthorizationManagerAuthorizationDidUpdateNotification() { 152 | DispatchQueue.main.async { 153 | self.tableView.reloadData() 154 | } 155 | } 156 | } 157 | 158 | extension MediaSearchTableViewController: UISearchResultsUpdating { 159 | func updateSearchResults(for searchController: UISearchController) { 160 | guard let searchString = searchController.searchBar.text else { 161 | return 162 | } 163 | 164 | if searchString == "" { 165 | self.setterQueue.sync { 166 | self.mediaItems = [] 167 | } 168 | } else { 169 | appleMusicManager.performAppleMusicCatalogSearch(with: searchString, 170 | countryCode: authorizationManager.cloudServiceStorefrontCountryCode, 171 | completion: { [weak self] (searchResults, error) in 172 | guard error == nil else { 173 | 174 | // Your application should handle these errors appropriately depending on the kind of error. 175 | self?.setterQueue.sync { 176 | self?.mediaItems = [] 177 | } 178 | 179 | let alertController: UIAlertController 180 | 181 | guard let error = error as NSError?, let underlyingError = error.userInfo[NSUnderlyingErrorKey] as? Error else { 182 | 183 | alertController = UIAlertController(title: "Error", 184 | message: "Encountered unexpected error.", 185 | preferredStyle: .alert) 186 | alertController.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil)) 187 | 188 | DispatchQueue.main.async { 189 | self?.present(alertController, animated: true, completion: nil) 190 | } 191 | 192 | return 193 | } 194 | 195 | alertController = UIAlertController(title: "Error", 196 | message: underlyingError.localizedDescription, 197 | preferredStyle: .alert) 198 | alertController.addAction(UIAlertAction(title: "Dismiss", style: .cancel, handler: nil)) 199 | 200 | DispatchQueue.main.async { 201 | self?.present(alertController, animated: true, completion: nil) 202 | } 203 | 204 | return 205 | } 206 | 207 | self?.setterQueue.sync { 208 | self?.mediaItems = searchResults 209 | } 210 | 211 | }) 212 | } 213 | } 214 | } 215 | 216 | extension MediaSearchTableViewController: UISearchBarDelegate { 217 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 218 | setterQueue.sync { 219 | self.mediaItems = [] 220 | } 221 | } 222 | } 223 | 224 | extension MediaSearchTableViewController: MediaSearchTableViewCellDelegate { 225 | func mediaSearchTableViewCell(_ mediaSearchTableViewCell: MediaItemTableViewCell, addToPlaylist mediaItem: MediaItem) { 226 | mediaLibraryManager.addItem(with: mediaItem.identifier) 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /Configuration/SampleCode.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // SampleCode.xcconfig 3 | // 4 | 5 | // The `SAMPLE_CODE_DISAMBIGUATOR` configuration is to make it easier to build 6 | // and run a sample code project. Once you set your project's development team, 7 | // you'll have a unique bundle identifier. This is because the bundle identifier 8 | // is derived based on the 'SAMPLE_CODE_DISAMBIGUATOR' value. Do not use this 9 | // approach in your own projects—it's only useful for sample code projects because 10 | // they are frequently downloaded and don't have a development team set. 11 | SAMPLE_CODE_DISAMBIGUATOR=${DEVELOPMENT_TEAM} 12 | -------------------------------------------------------------------------------- /LICENSE/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2017 Apple Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adding Content to Apple Music 2 | 3 | Demonstrates how to add content from the Apple Music catalog to the iCloud Music Library. 4 | 5 | ## Overview 6 | 7 | This sample shows how to use the MediaPlayer and StoreKit frameworks as well as the Apple Music Web Service to do the following: 8 | 9 | * Request access to the iOS device's Media and Apple Music. 10 | * Present the Apple Music subscriber setup flow if the currently signed in iTunes Store account is elgible. 11 | * Search the Apple Music catalog for songs and albums using the Apple Music Web Service. 12 | * Create a new [`MPMediaPlaylist`](https://developer.apple.com/documentation/mediaplayer/mpmediaplaylist) locally or in the user's iCloud Music Library and add items to it. 13 | * Playback items from the Apple Music catalog or play the [`MPMediaPlaylist`](https://developer.apple.com/documentation/mediaplayer/mpmediaplaylist) created by the application. 14 | 15 | ## Getting Started 16 | 17 | You use a developer token to authenticate yourself as a trusted developer and member of the Apple Developer Program. A developer token is required in the header of every Apple Music API request. To create a developer token, first create a MusicKit signing key in your developer account, create a JSON Web Token (JWT) in the format Apple expects, and then sign it with the MusicKit signing key. For more information about this process and how to create a developer token please see the following documentation: 18 | 19 | * [Apple Music API Reference - Get Keys and Create Tokens](https://developer.apple.com/go/?id=apple-music-keys-and-tokens). 20 | 21 | Once you have a developer token, you need to update the `AppleMusicManager.fetchDeveloperToken()` method in `AppleMusicManager.swift` to retrieve your valid developer token. 22 | 23 | ``` swift 24 | func fetchDeveloperToken() -> String? { 25 | 26 | // MARK: ADAPT: YOU MUST IMPLEMENT THIS METHOD 27 | let developerAuthenticationToken: String? = nil 28 | return developerAuthenticationToken 29 | } 30 | ``` 31 | 32 | Keep in mind, you should not hardcode the value of the developer token in your application. This is so that if you need to generate a new developer token you are able to without having to submit a new version of your application to the App Store. 33 | 34 | ## Requesting Authorization 35 | 36 | Before interacting with these APIs your application needs to request authorization from the user to interact with the device media library and with Apple Music. 37 | 38 | There are two different authorizations that iOS applications can request. Depending on your application's usecase, you may only need to request one of the above or request both of them. 39 | 40 | ### Media Library Authorization 41 | 42 | If your application wants to access the items in the user's media library then you should request authorization using the [`MPMediaLibrary`](https://developer.apple.com/documentation/mediaplayer/mpmedialibrary) APIs. 43 | 44 | To query your application's current [`MPMediaLibraryAuthorizationStatus`](https://developer.apple.com/documentation/mediaplayer/mpmedialibraryauthorizationstatus), you can call [`MPMediaLibrary.authorizationStatus()`](https://developer.apple.com/documentation/mediaplayer/mpmedialibrary/1621282-authorizationstatus). 45 | 46 | ``` swift 47 | guard MPMediaLibrary.authorizationStatus() == .notDetermined else { return } 48 | ``` 49 | 50 | If the authorization status is `.notDetermined` then your application should request authorization by calling [`MPMediaLibrary.requestAuthorization(_:)`](https://developer.apple.com/documentation/mediaplayer/mpmedialibrary/1621276-requestauthorization). 51 | 52 | ``` swift 53 | MPMediaLibrary.requestAuthorization { (_) in 54 | NotificationCenter.default.post(name: AuthorizationManager.cloudServiceDidUpdateNotification, object: nil) 55 | } 56 | ``` 57 | 58 | ### Cloud Service Authorization 59 | 60 | If your application wants to be able to playback items from the Apple Music catalog or add items to the user's iCloud Music Library then you should request authorization using the `SKCloudServiceController` APIs. 61 | 62 | To query your application's current [`SKCloudServiceAuthorizationStatus`](https://developer.apple.com/documentation/storekit/skcloudserviceauthorizationstatus), you can call [`SKCloudServiceController.authorizationStatus()`](https://developer.apple.com/documentation/storekit/skcloudservicecontroller/1620631-authorizationstatus). 63 | 64 | ``` swift 65 | guard SKCloudServiceController.authorizationStatus() == .notDetermined else { return } 66 | ``` 67 | 68 | If the authorization status is `.notDetermined` then your application should request authorization by calling [`SKCloudServiceController.requestAuthorization(_:)`](https://developer.apple.com/documentation/storekit/skcloudservicecontroller/1620609-requestauthorization). 69 | 70 | ``` swift 71 | SKCloudServiceController.requestAuthorization { [weak self] (authorizationStatus) in 72 | switch authorizationStatus { 73 | case .authorized: 74 | self?.requestCloudServiceCapabilities() 75 | self?.requestUserToken() 76 | default: 77 | break 78 | } 79 | 80 | NotificationCenter.default.post(name: AuthorizationManager.authorizationDidUpdateNotification, object: nil) 81 | } 82 | ``` 83 | 84 | Once your application has the `.authorized` status, you can query the the device for more information about the capabilities associated with the device. These capabilities are represented as [`SKCloudServiceCapability`](https://developer.apple.com/documentation/storekit/skcloudservicecapability) and can be queried by calling [`requestCapabilities(completionHandler:)`](https://developer.apple.com/documentation/storekit/skcloudservicecontroller/1620610-requestcapabilities) on an instance of [`SKCloudServiceController`](https://developer.apple.com/documentation/storekit/skcloudservicecontroller). 85 | 86 | ```swift 87 | let controller = SKCloudServiceController() 88 | controller.requestCapabilities(completionHandler: { (cloudServiceCapability, error) in 89 | guard error == nil else { 90 | // Handle Error accordingly, see SKError.h for error codes. 91 | } 92 | 93 | if cloudServiceCapabilities.contains(.addToCloudMusicLibrary) { 94 | // The application can add items to the iCloud Music Library. 95 | } 96 | 97 | if cloudServiceCapabilities.contains(.musicCatalogPlayback) { 98 | // The application can playback items from the Apple Music catalog. 99 | } 100 | 101 | if cloudServiceCapabilities.contains(.musicCatalogSubscriptionEligible) { 102 | // The iTunes Store account is currently elgible for and Apple Music Subscription trial. 103 | } 104 | }) 105 | ``` 106 | 107 | ## Requesting a Music User Token 108 | 109 | If your application makes calls to the Apple Music API for personalized requests that return user-specific data, your request will need to include a music user token. To create a music user token you first need to have a valid developer token as discussed above in the "Getting Started" section. 110 | 111 | Once you have a developer token, you can use the native APIs availalbe on the `SKCloudServiceController` class as demonstrated below: 112 | 113 | ```swift 114 | let completionHandler: (String?, Error?) -> Void = { [weak self] (token, error) in 115 | guard error == nil else { 116 | // Handle Error accordingly, see SKError.h for error codes. 117 | } 118 | 119 | guard let token = token else { 120 | print("Unexpected value from SKCloudServiceController for user token.") 121 | return 122 | } 123 | 124 | self?.userToken = token 125 | } 126 | 127 | if #available(iOS 11.0, *) { 128 | cloudServiceController.requestUserToken(forDeveloperToken: developerToken, completionHandler: completionHandler) 129 | } else { 130 | cloudServiceController.requestPersonalizationToken(forClientToken: developerToken, withCompletionHandler: completionHandler) 131 | } 132 | ``` 133 | 134 | Once you have a valid music user token, your application should cache it for future use in personalized requests for the Apple Music API. For additional information about how the music user token is used in making requests, please see the following documentation: 135 | 136 | * [Apple Music API Reference - Authenticate Requests](https://developer.apple.com/library/content/documentation/NetworkingInternetWeb/Conceptual/AppleMusicWebServicesReference/SetUpWebServices.html#//apple_ref/doc/uid/TP40017625-CH2-SW7). 137 | 138 | ## Creating and Adding Items to a Media Playlist 139 | 140 | After your application is authorized to access the iCloud Music Library, you can use the `MPMediaLibrary` APIs to create or retrieve an existing [`MPMediaPlaylist`](https://developer.apple.com/documentation/mediaplayer/mpmediaplaylist). 141 | 142 | To create or retrieve an [`MPMediaPlaylist`](https://developer.apple.com/documentation/mediaplayer/mpmediaplaylist), use the [`MPMediaLibrary.getPlaylist(with:creationMetadata:completionHandler:)`](https://developer.apple.com/documentation/mediaplayer/mpmedialibrary/1621273-getplaylist) as demonstrated below: 143 | 144 | ```swift 145 | /* 146 | Create an instance of `UUID` to identify the new playlist. If you wish to be able to retrieve this playlist in the future, 147 | save this UUID in your application for future use. 148 | */ 149 | let playlistUUID = UUID() 150 | 151 | // Create an instance of `MPMediaPlaylistCreationMetadata`, this represents the metadata to associate with the new playlist. 152 | var playlistCreationMetadata = MPMediaPlaylistCreationMetadata(name: "My Playlist") 153 | playlistCreationMetadata.descriptionText = "This playlist contains awesome items." 154 | 155 | // Request the new or existing playlist from the device. 156 | MPMediaLibrary.default().getPlaylist(with: playlistUUID, creationMetadata: playlistCreationMetadata) { (playlist, error) in 157 | guard error == nil else { 158 | // Handle Error accordingly, see MPError.h for error codes. 159 | } 160 | 161 | self.mediaPlaylist = playlist 162 | } 163 | ``` 164 | 165 | Once you have an instance of [`MPMediaPlaylist`](https://developer.apple.com/documentation/mediaplayer/mpmediaplaylist), you can then add items to the playlist using the [`MPMediaPlaylist.addItem(withProductID:completionHandler:)`](https://developer.apple.com/documentation/mediaplayer/mpmediaplaylist/1618706-additem) API. 166 | 167 | ``` swift 168 | mediaPlaylist.addItem(withProductID: identifier, completionHandler: { (error) in 169 | guard error == nil else { 170 | fatalError("An error occurred while adding an item to the playlist: \(error!.localizedDescription)") 171 | } 172 | 173 | NotificationCenter.default.post(name: MediaLibraryManager.libraryDidUpdate, object: nil) 174 | }) 175 | ``` 176 | 177 | ## Playing Items from the Apple Music catalog 178 | 179 | After your application is authorized and has the `SKCloudServiceCapability.musicCatalogPlayback` capability, you can play one or more items from the Apple Music catalog or the iCloud Music Library using the `MPMusicPlayerController` APIs. 180 | 181 | If you have items from the Apple Music API that you wish to play, you can use the [`MPMusicPlayerController.setQueueWithStoreIDs(_:)`](https://developer.apple.com/documentation/mediaplayer/mpmusicplayercontroller/1624253-setqueuewithstoreids) API and pass in an array of strings that represent the id of the resource from the Apple Music API. 182 | 183 | ``` swift 184 | musicPlayerController.setQueue(with: [itemID]) 185 | 186 | musicPlayerController.play() 187 | ``` 188 | 189 | If you have an [`MPMediaPlaylist`](https://developer.apple.com/documentation/mediaplayer/mpmediaplaylist) or `MPMediaItemCollection` that you wish to play, you can use the [`MPMusicPlayerController.setQueue(with:)`](https://developer.apple.com/documentation/mediaplayer/mpmusicplayercontroller/1624171-setqueue) API. 190 | 191 | ``` swift 192 | musicPlayerController.setQueue(with: itemCollection) 193 | 194 | musicPlayerController.play() 195 | ``` 196 | --------------------------------------------------------------------------------