├── .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 |
--------------------------------------------------------------------------------