├── .gitattributes
├── MusicPlayer
├── Assets.xcassets
│ ├── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
├── MusicPlayer.entitlements
├── Song.swift
├── ContentView.swift
├── AppDelegate.swift
├── Base.lproj
│ └── LaunchScreen.storyboard
├── Info.plist
├── SearchView.swift
├── SceneDelegate.swift
├── AppleMusicAPI.swift
├── PlayerView.swift
└── SwiftyJSON.swift
├── MusicPlayer.xcodeproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── saikambampati.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── swiftpm
│ │ └── Package.resolved
├── xcuserdata
│ └── saikambampati.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
└── project.pbxproj
├── README.md
├── musictoken.py
└── LICENSE
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/MusicPlayer/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/MusicPlayer/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/MusicPlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/MusicPlayer.xcodeproj/project.xcworkspace/xcuserdata/saikambampati.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/appcoda/MusicKitPlayer/HEAD/MusicPlayer.xcodeproj/project.xcworkspace/xcuserdata/saikambampati.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Working with MusicKit and SwiftUI
2 |
3 | In this tutorial, we will walk you through the MusicKit framework by building a Music Player using SwiftUI. For the full tutorial, please check it out here:
4 |
5 | https://www.appcoda.com/musickit-music-player-swiftui/
6 |
--------------------------------------------------------------------------------
/MusicPlayer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/MusicPlayer/MusicPlayer.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.network.client
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/MusicPlayer.xcodeproj/xcuserdata/saikambampati.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | MusicPlayer.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/MusicPlayer/Song.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Song.swift
3 | // MusicPlayer
4 | //
5 | // Created by Sai Kambampati on 5/30/20.
6 | // Copyright © 2020 Sai Kambmapati. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | struct Song {
12 | var id: String
13 | var name: String
14 | var artistName: String
15 | var artworkURL: String
16 |
17 | init(id: String, name: String, artistName: String, artworkURL: String) {
18 | self.id = id
19 | self.name = name
20 | self.artworkURL = artworkURL
21 | self.artistName = artistName
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/MusicPlayer.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "SDWebImage",
6 | "repositoryURL": "https://github.com/SDWebImage/SDWebImage.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "5818cf70fde7e9e07e24196d0d54350bc8526f26",
10 | "version": "5.8.0"
11 | }
12 | },
13 | {
14 | "package": "SDWebImageSwiftUI",
15 | "repositoryURL": "https://github.com/SDWebImage/SDWebImageSwiftUI",
16 | "state": {
17 | "branch": null,
18 | "revision": "4c7f169e39bc35d6b80d42b8eb8301bee9cd0907",
19 | "version": "1.5.0"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/musictoken.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import jwt
3 |
4 |
5 | secret = """-----BEGIN PRIVATE KEY-----
6 | PASTE
7 | YOUR
8 | KEY
9 | ER
10 | -----END PRIVATE KEY-----"""
11 | keyId = "ABCDEFGHIJ"
12 | teamId = "0123456789"
13 | alg = 'ES256'
14 |
15 | time_now = datetime.datetime.now()
16 | time_expired = datetime.datetime.now() + datetime.timedelta(hours=4380)
17 |
18 | headers = {
19 | "alg": alg,
20 | "kid": keyId
21 | }
22 |
23 | payload = {
24 | "iss": teamId,
25 | "exp": int(time_expired.strftime("%s")),
26 | "iat": int(time_now.strftime("%s"))
27 | }
28 |
29 |
30 | if __name__ == "__main__":
31 | """Create an auth token"""
32 | token = jwt.encode(payload, secret, algorithm=alg, headers=headers)
33 |
34 | print "----TOKEN----"
35 | print token
36 |
37 | print "----CURL----"
38 | print "curl -v -H 'Authorization: Bearer %s' \"https://api.music.apple.com/v1/catalog/us/artists/36954\" " % (token)
39 |
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Simon Ng
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MusicPlayer/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // MusicPlayer
4 | //
5 | // Created by Sai Kambampati on 3/18/20.
6 | // Copyright © 2020 Sai Kambmapati. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import StoreKit
11 | import MediaPlayer
12 |
13 | struct ContentView: View {
14 | @State private var selection = 0
15 | @State private var musicPlayer = MPMusicPlayerController.applicationMusicPlayer
16 | @State private var currentSong = Song(id: "", name: "", artistName: "", artworkURL: "")
17 |
18 | var body: some View {
19 | TabView(selection: $selection) {
20 | PlayerView(musicPlayer: self.$musicPlayer, currentSong: self.$currentSong)
21 | .tag(0)
22 | .tabItem {
23 | VStack {
24 | Image(systemName: "music.note")
25 | Text("Player")
26 | }
27 | }
28 | SearchView(musicPlayer: self.$musicPlayer, currentSong: self.$currentSong)
29 | .tag(1)
30 | .tabItem {
31 | VStack {
32 | Image(systemName: "magnifyingglass")
33 | Text("Search")
34 | }
35 | }
36 | }
37 | .accentColor(.pink)
38 | }
39 | }
40 |
41 | struct ContentView_Previews: PreviewProvider {
42 | static var previews: some View {
43 | ContentView()
44 | .colorScheme(.dark)
45 | .previewDevice(.init(rawValue: "iPhone 11"))
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/MusicPlayer/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // MusicPlayer
4 | //
5 | // Created by Sai Kambampati on 3/18/20.
6 | // Copyright © 2020 Sai Kambmapati. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 |
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | // Override point for customization after application launch.
18 | return true
19 | }
20 |
21 | // MARK: UISceneSession Lifecycle
22 |
23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
24 | // Called when a new scene session is being created.
25 | // Use this method to select a configuration to create the new scene with.
26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
27 | }
28 |
29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
30 | // Called when the user discards a scene session.
31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
33 | }
34 |
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/MusicPlayer/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 |
--------------------------------------------------------------------------------
/MusicPlayer/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 | }
--------------------------------------------------------------------------------
/MusicPlayer/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | NSAppleMusicUsageDescription
6 | Access to your Apple Music account is critical for the functionality of the app
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UIApplicationSceneManifest
26 |
27 | UIApplicationSupportsMultipleScenes
28 |
29 | UISceneConfigurations
30 |
31 | UIWindowSceneSessionRoleApplication
32 |
33 |
34 | UISceneConfigurationName
35 | Default Configuration
36 | UISceneDelegateClassName
37 | $(PRODUCT_MODULE_NAME).SceneDelegate
38 |
39 |
40 |
41 |
42 | UILaunchStoryboardName
43 | LaunchScreen
44 | UIRequiredDeviceCapabilities
45 |
46 | armv7
47 |
48 | UIStatusBarTintParameters
49 |
50 | UINavigationBar
51 |
52 | Style
53 | UIBarStyleDefault
54 | Translucent
55 |
56 |
57 |
58 | UISupportedInterfaceOrientations
59 |
60 | UIInterfaceOrientationPortrait
61 | UIInterfaceOrientationLandscapeLeft
62 | UIInterfaceOrientationLandscapeRight
63 |
64 | UISupportedInterfaceOrientations~ipad
65 |
66 | UIInterfaceOrientationPortrait
67 | UIInterfaceOrientationPortraitUpsideDown
68 | UIInterfaceOrientationLandscapeLeft
69 | UIInterfaceOrientationLandscapeRight
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/MusicPlayer/SearchView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchView.swift
3 | // MusicPlayer
4 | //
5 | // Created by Sai Kambampati on 3/18/20.
6 | // Copyright © 2020 Sai Kambmapati. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import StoreKit
11 | import MediaPlayer
12 | import SDWebImageSwiftUI
13 |
14 | struct SearchView: View {
15 | @State private var searchText = ""
16 | @State private var searchResults = [Song]()
17 | @Binding var musicPlayer: MPMusicPlayerController
18 | @Binding var currentSong: Song
19 |
20 | var body: some View {
21 | VStack {
22 | TextField("Search Songs", text: $searchText, onCommit: {
23 | UIApplication.shared.resignFirstResponder()
24 | if self.searchText.isEmpty {
25 | self.searchResults = []
26 | } else {
27 | SKCloudServiceController.requestAuthorization { (status) in
28 | if status == .authorized {
29 | self.searchResults = AppleMusicAPI().searchAppleMusic(self.searchText)
30 | }
31 | }
32 | }
33 | })
34 | .textFieldStyle(RoundedBorderTextFieldStyle())
35 | .padding(.horizontal, 16)
36 | .accentColor(.pink)
37 |
38 | List {
39 | ForEach(searchResults, id:\.id) { song in
40 | HStack {
41 | WebImage(url: URL(string: song.artworkURL.replacingOccurrences(of: "{w}", with: "80").replacingOccurrences(of: "{h}", with: "80")))
42 | .resizable()
43 | .frame(width: 40, height: 40)
44 | .cornerRadius(5)
45 | .shadow(radius: 2)
46 |
47 | VStack(alignment: .leading) {
48 | Text(song.name)
49 | .font(.headline)
50 | Text(song.artistName)
51 | .font(.caption)
52 | .foregroundColor(.secondary)
53 | }
54 | Spacer()
55 | Button(action: {
56 | self.currentSong = song
57 | self.musicPlayer.setQueue(with: [song.id])
58 | self.musicPlayer.play()
59 | }) {
60 | Image(systemName: "play.fill")
61 | .foregroundColor(.pink)
62 | }
63 | }
64 | }
65 | }
66 | .accentColor(.pink)
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/MusicPlayer/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // MusicPlayer
4 | //
5 | // Created by Sai Kambampati on 3/18/20.
6 | // Copyright © 2020 Sai Kambmapati. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import SwiftUI
11 |
12 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
18 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
19 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
20 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
21 |
22 | // Create the SwiftUI view that provides the window contents.
23 | let contentView = ContentView()
24 |
25 | // Use a UIHostingController as window root view controller.
26 | if let windowScene = scene as? UIWindowScene {
27 | let window = UIWindow(windowScene: windowScene)
28 | window.rootViewController = UIHostingController(rootView: contentView)
29 | self.window = window
30 | window.makeKeyAndVisible()
31 | }
32 | }
33 |
34 | func sceneDidDisconnect(_ scene: UIScene) {
35 | // Called as the scene is being released by the system.
36 | // This occurs shortly after the scene enters the background, or when its session is discarded.
37 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
38 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
39 | }
40 |
41 | func sceneDidBecomeActive(_ scene: UIScene) {
42 | // Called when the scene has moved from an inactive state to an active state.
43 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
44 | }
45 |
46 | func sceneWillResignActive(_ scene: UIScene) {
47 | // Called when the scene will move from an active state to an inactive state.
48 | // This may occur due to temporary interruptions (ex. an incoming phone call).
49 | }
50 |
51 | func sceneWillEnterForeground(_ scene: UIScene) {
52 | // Called as the scene transitions from the background to the foreground.
53 | // Use this method to undo the changes made on entering the background.
54 | }
55 |
56 | func sceneDidEnterBackground(_ scene: UIScene) {
57 | // Called as the scene transitions from the foreground to the background.
58 | // Use this method to save data, release shared resources, and store enough scene-specific state information
59 | // to restore the scene back to its current state.
60 | }
61 |
62 |
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/MusicPlayer/AppleMusicAPI.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppleMusicAPI.swift
3 | // MusicPlayer
4 | //
5 | // Created by Sai Kambampati on 5/30/20.
6 | // Copyright © 2020 Sai Kambmapati. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import StoreKit
11 |
12 | class AppleMusicAPI {
13 | let developerToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6Ikw1WjQ3TEQ0N1cifQ.eyJpc3MiOiJYSllNRzc2MjI2IiwiZXhwIjoxNjA2NzYwNjIwLCJpYXQiOjE1OTA5ODkwMTl9.3D0q4GwH4wucCVnnEcuXcSDjNZJJEHCJydruCdAbueAMfDhpdVea4Dvi38DXHGMrA_Mew1JyedP_M-78-T4cjw"
14 |
15 | func getUserToken() -> String {
16 | var userToken = String()
17 |
18 | // 1
19 | let lock = DispatchSemaphore(value: 0)
20 |
21 | // 2
22 | SKCloudServiceController().requestUserToken(forDeveloperToken: developerToken) { (receivedToken, error) in
23 | // 3
24 | guard error == nil else { return }
25 | if let token = receivedToken {
26 | userToken = token
27 | lock.signal()
28 | }
29 | }
30 |
31 | // 4
32 | lock.wait()
33 | return userToken
34 | }
35 |
36 | func fetchStorefrontID() -> String {
37 | let lock = DispatchSemaphore(value: 0)
38 | var storefrontID: String!
39 |
40 | let musicURL = URL(string: "https://api.music.apple.com/v1/me/storefront")!
41 | var musicRequest = URLRequest(url: musicURL)
42 | musicRequest.httpMethod = "GET"
43 | musicRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization")
44 | musicRequest.addValue(getUserToken(), forHTTPHeaderField: "Music-User-Token")
45 |
46 | URLSession.shared.dataTask(with: musicRequest) { (data, response, error) in
47 | guard error == nil else { return }
48 |
49 | if let json = try? JSON(data: data!) {
50 | let result = (json["data"]).array!
51 | let id = (result[0].dictionaryValue)["id"]!
52 | storefrontID = id.stringValue
53 | lock.signal()
54 | }
55 | }.resume()
56 |
57 | lock.wait()
58 | return storefrontID
59 | }
60 |
61 | func searchAppleMusic(_ searchTerm: String!) -> [Song] {
62 | let lock = DispatchSemaphore(value: 0)
63 | var songs = [Song]()
64 |
65 | let musicURL = URL(string: "https://api.music.apple.com/v1/catalog/\(fetchStorefrontID())/search?term=\(searchTerm.replacingOccurrences(of: " ", with: "+"))&types=songs&limit=25")!
66 | var musicRequest = URLRequest(url: musicURL)
67 | musicRequest.httpMethod = "GET"
68 | musicRequest.addValue("Bearer \(developerToken)", forHTTPHeaderField: "Authorization")
69 | musicRequest.addValue(getUserToken(), forHTTPHeaderField: "Music-User-Token")
70 |
71 | URLSession.shared.dataTask(with: musicRequest) { (data, response, error) in
72 | guard error == nil else { return }
73 | if let json = try? JSON(data: data!) {
74 | let result = (json["results"]["songs"]["data"]).array!
75 | for song in result {
76 | let attributes = song["attributes"]
77 | let currentSong = Song(id: attributes["playParams"]["id"].string!, name: attributes["name"].string!, artistName: attributes["artistName"].string!, artworkURL: attributes["artwork"]["url"].string!)
78 | songs.append(currentSong)
79 | }
80 | lock.signal()
81 | } else {
82 | lock.signal()
83 | }
84 | }.resume()
85 |
86 | lock.wait()
87 | return songs
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/MusicPlayer/PlayerView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlayerView.swift
3 | // MusicPlayer
4 | //
5 | // Created by Sai Kambampati on 3/18/20.
6 | // Copyright © 2020 Sai Kambmapati. All rights reserved.
7 | //
8 |
9 | import SwiftUI
10 | import MediaPlayer
11 | import SDWebImageSwiftUI
12 |
13 | struct PlayerView: View {
14 | @Binding var musicPlayer: MPMusicPlayerController
15 | @State private var isPlaying = false
16 | @Binding var currentSong: Song
17 |
18 | var body: some View {
19 | GeometryReader { geometry in
20 | VStack(spacing: 24) {
21 | WebImage(url: URL(string: self.currentSong.artworkURL.replacingOccurrences(of: "{w}", with: "\(Int(geometry.size.width - 24) * 2)").replacingOccurrences(of: "{h}", with: "\(Int(geometry.size.width - 24) * 2)")))
22 | .resizable()
23 | .frame(width: geometry.size.width - 24, height: geometry.size.width - 24)
24 | .cornerRadius(20)
25 | .shadow(radius: 10)
26 |
27 | VStack(spacing: 8) {
28 | Text(self.musicPlayer.nowPlayingItem?.title ?? "Not Playing")
29 | .font(Font.system(.title).bold())
30 | .multilineTextAlignment(.center)
31 | Text(self.musicPlayer.nowPlayingItem?.artist ?? "")
32 | .font(.system(.headline))
33 | }
34 |
35 | HStack(spacing: 40) {
36 | Button(action: {
37 | if self.musicPlayer.currentPlaybackTime < 5 {
38 | self.musicPlayer.skipToPreviousItem()
39 | } else {
40 | self.musicPlayer.skipToBeginning()
41 | }
42 | }) {
43 | ZStack {
44 | Circle()
45 | .frame(width: 80, height: 80)
46 | .accentColor(.pink)
47 | .shadow(radius: 10)
48 | Image(systemName: "backward.fill")
49 | .foregroundColor(.white)
50 | .font(.system(.title))
51 | }
52 | }
53 |
54 | Button(action: {
55 | if self.musicPlayer.playbackState == .paused || self.musicPlayer.playbackState == .stopped {
56 | self.musicPlayer.play()
57 | self.isPlaying = true
58 | } else {
59 | self.musicPlayer.pause()
60 | self.isPlaying = false
61 | }
62 | }) {
63 | ZStack {
64 | Circle()
65 | .frame(width: 80, height: 80)
66 | .accentColor(.pink)
67 | .shadow(radius: 10)
68 | Image(systemName: self.isPlaying ? "pause.fill" : "play.fill")
69 | .foregroundColor(.white)
70 | .font(.system(.title))
71 | }
72 | }
73 |
74 | Button(action: {
75 | self.musicPlayer.skipToNextItem()
76 | }) {
77 | ZStack {
78 | Circle()
79 | .frame(width: 80, height: 80)
80 | .accentColor(.pink)
81 | .shadow(radius: 10)
82 | Image(systemName: "forward.fill")
83 | .foregroundColor(.white)
84 | .font(.system(.title))
85 | }
86 | }
87 | }
88 |
89 | }
90 | }
91 | .onAppear() {
92 | if self.musicPlayer.playbackState == .playing {
93 | self.isPlaying = true
94 | } else {
95 | self.isPlaying = false
96 | }
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/MusicPlayer.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 8B0ED903248D968B0055C5EB /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 8B0ED902248D968B0055C5EB /* SDWebImageSwiftUI */; };
11 | 8B3DC4B92422A2810067355E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B3DC4B82422A2810067355E /* AppDelegate.swift */; };
12 | 8B3DC4BB2422A2810067355E /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B3DC4BA2422A2810067355E /* SceneDelegate.swift */; };
13 | 8B3DC4BD2422A2810067355E /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B3DC4BC2422A2810067355E /* ContentView.swift */; };
14 | 8B3DC4BF2422A2820067355E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B3DC4BE2422A2820067355E /* Assets.xcassets */; };
15 | 8B3DC4C22422A2820067355E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8B3DC4C12422A2820067355E /* Preview Assets.xcassets */; };
16 | 8B3DC4C52422A2820067355E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8B3DC4C32422A2820067355E /* LaunchScreen.storyboard */; };
17 | 8B58077424838BE800C6888B /* AppleMusicAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B58077324838BE800C6888B /* AppleMusicAPI.swift */; };
18 | 8B58077624838D6E00C6888B /* Song.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B58077524838D6E00C6888B /* Song.swift */; };
19 | 8B5807782483991A00C6888B /* SwiftyJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B5807772483991A00C6888B /* SwiftyJSON.swift */; };
20 | 8B708D642422FB9700CE1598 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B708D632422FB9700CE1598 /* PlayerView.swift */; };
21 | 8B708D662423108200CE1598 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B708D652423108200CE1598 /* SearchView.swift */; };
22 | /* End PBXBuildFile section */
23 |
24 | /* Begin PBXFileReference section */
25 | 8B3DC4B52422A2810067355E /* MusicPlayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MusicPlayer.app; sourceTree = BUILT_PRODUCTS_DIR; };
26 | 8B3DC4B82422A2810067355E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
27 | 8B3DC4BA2422A2810067355E /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
28 | 8B3DC4BC2422A2810067355E /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
29 | 8B3DC4BE2422A2820067355E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
30 | 8B3DC4C12422A2820067355E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
31 | 8B3DC4C42422A2820067355E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
32 | 8B3DC4C62422A2820067355E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
33 | 8B58077324838BE800C6888B /* AppleMusicAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleMusicAPI.swift; sourceTree = ""; };
34 | 8B58077524838D6E00C6888B /* Song.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Song.swift; sourceTree = ""; };
35 | 8B5807772483991A00C6888B /* SwiftyJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftyJSON.swift; sourceTree = ""; };
36 | 8B708D632422FB9700CE1598 /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; };
37 | 8B708D652423108200CE1598 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = ""; };
38 | 8BD60D7C2484C286000DC1DF /* MusicPlayer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MusicPlayer.entitlements; sourceTree = ""; };
39 | /* End PBXFileReference section */
40 |
41 | /* Begin PBXFrameworksBuildPhase section */
42 | 8B3DC4B22422A2810067355E /* Frameworks */ = {
43 | isa = PBXFrameworksBuildPhase;
44 | buildActionMask = 2147483647;
45 | files = (
46 | 8B0ED903248D968B0055C5EB /* SDWebImageSwiftUI in Frameworks */,
47 | );
48 | runOnlyForDeploymentPostprocessing = 0;
49 | };
50 | /* End PBXFrameworksBuildPhase section */
51 |
52 | /* Begin PBXGroup section */
53 | 8B3DC4AC2422A2810067355E = {
54 | isa = PBXGroup;
55 | children = (
56 | 8B3DC4B72422A2810067355E /* MusicPlayer */,
57 | 8B3DC4B62422A2810067355E /* Products */,
58 | );
59 | sourceTree = "";
60 | };
61 | 8B3DC4B62422A2810067355E /* Products */ = {
62 | isa = PBXGroup;
63 | children = (
64 | 8B3DC4B52422A2810067355E /* MusicPlayer.app */,
65 | );
66 | name = Products;
67 | sourceTree = "";
68 | };
69 | 8B3DC4B72422A2810067355E /* MusicPlayer */ = {
70 | isa = PBXGroup;
71 | children = (
72 | 8BD60D7C2484C286000DC1DF /* MusicPlayer.entitlements */,
73 | 8B3DC4B82422A2810067355E /* AppDelegate.swift */,
74 | 8B3DC4BA2422A2810067355E /* SceneDelegate.swift */,
75 | 8B3DC4BC2422A2810067355E /* ContentView.swift */,
76 | 8B58077324838BE800C6888B /* AppleMusicAPI.swift */,
77 | 8B58077524838D6E00C6888B /* Song.swift */,
78 | 8B708D632422FB9700CE1598 /* PlayerView.swift */,
79 | 8B708D652423108200CE1598 /* SearchView.swift */,
80 | 8B5807772483991A00C6888B /* SwiftyJSON.swift */,
81 | 8B3DC4BE2422A2820067355E /* Assets.xcassets */,
82 | 8B3DC4C32422A2820067355E /* LaunchScreen.storyboard */,
83 | 8B3DC4C62422A2820067355E /* Info.plist */,
84 | 8B3DC4C02422A2820067355E /* Preview Content */,
85 | );
86 | path = MusicPlayer;
87 | sourceTree = "";
88 | };
89 | 8B3DC4C02422A2820067355E /* Preview Content */ = {
90 | isa = PBXGroup;
91 | children = (
92 | 8B3DC4C12422A2820067355E /* Preview Assets.xcassets */,
93 | );
94 | path = "Preview Content";
95 | sourceTree = "";
96 | };
97 | /* End PBXGroup section */
98 |
99 | /* Begin PBXNativeTarget section */
100 | 8B3DC4B42422A2810067355E /* MusicPlayer */ = {
101 | isa = PBXNativeTarget;
102 | buildConfigurationList = 8B3DC4C92422A2820067355E /* Build configuration list for PBXNativeTarget "MusicPlayer" */;
103 | buildPhases = (
104 | 8B3DC4B12422A2810067355E /* Sources */,
105 | 8B3DC4B22422A2810067355E /* Frameworks */,
106 | 8B3DC4B32422A2810067355E /* Resources */,
107 | );
108 | buildRules = (
109 | );
110 | dependencies = (
111 | );
112 | name = MusicPlayer;
113 | packageProductDependencies = (
114 | 8B0ED902248D968B0055C5EB /* SDWebImageSwiftUI */,
115 | );
116 | productName = MusicPlayer;
117 | productReference = 8B3DC4B52422A2810067355E /* MusicPlayer.app */;
118 | productType = "com.apple.product-type.application";
119 | };
120 | /* End PBXNativeTarget section */
121 |
122 | /* Begin PBXProject section */
123 | 8B3DC4AD2422A2810067355E /* Project object */ = {
124 | isa = PBXProject;
125 | attributes = {
126 | LastSwiftUpdateCheck = 1130;
127 | LastUpgradeCheck = 1130;
128 | ORGANIZATIONNAME = "Sai Kambmapati";
129 | TargetAttributes = {
130 | 8B3DC4B42422A2810067355E = {
131 | CreatedOnToolsVersion = 11.3.1;
132 | };
133 | };
134 | };
135 | buildConfigurationList = 8B3DC4B02422A2810067355E /* Build configuration list for PBXProject "MusicPlayer" */;
136 | compatibilityVersion = "Xcode 9.3";
137 | developmentRegion = en;
138 | hasScannedForEncodings = 0;
139 | knownRegions = (
140 | en,
141 | Base,
142 | );
143 | mainGroup = 8B3DC4AC2422A2810067355E;
144 | packageReferences = (
145 | 8B0ED901248D968B0055C5EB /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */,
146 | );
147 | productRefGroup = 8B3DC4B62422A2810067355E /* Products */;
148 | projectDirPath = "";
149 | projectRoot = "";
150 | targets = (
151 | 8B3DC4B42422A2810067355E /* MusicPlayer */,
152 | );
153 | };
154 | /* End PBXProject section */
155 |
156 | /* Begin PBXResourcesBuildPhase section */
157 | 8B3DC4B32422A2810067355E /* Resources */ = {
158 | isa = PBXResourcesBuildPhase;
159 | buildActionMask = 2147483647;
160 | files = (
161 | 8B3DC4C52422A2820067355E /* LaunchScreen.storyboard in Resources */,
162 | 8B3DC4C22422A2820067355E /* Preview Assets.xcassets in Resources */,
163 | 8B3DC4BF2422A2820067355E /* Assets.xcassets in Resources */,
164 | );
165 | runOnlyForDeploymentPostprocessing = 0;
166 | };
167 | /* End PBXResourcesBuildPhase section */
168 |
169 | /* Begin PBXSourcesBuildPhase section */
170 | 8B3DC4B12422A2810067355E /* Sources */ = {
171 | isa = PBXSourcesBuildPhase;
172 | buildActionMask = 2147483647;
173 | files = (
174 | 8B58077624838D6E00C6888B /* Song.swift in Sources */,
175 | 8B708D662423108200CE1598 /* SearchView.swift in Sources */,
176 | 8B58077424838BE800C6888B /* AppleMusicAPI.swift in Sources */,
177 | 8B5807782483991A00C6888B /* SwiftyJSON.swift in Sources */,
178 | 8B3DC4B92422A2810067355E /* AppDelegate.swift in Sources */,
179 | 8B3DC4BB2422A2810067355E /* SceneDelegate.swift in Sources */,
180 | 8B3DC4BD2422A2810067355E /* ContentView.swift in Sources */,
181 | 8B708D642422FB9700CE1598 /* PlayerView.swift in Sources */,
182 | );
183 | runOnlyForDeploymentPostprocessing = 0;
184 | };
185 | /* End PBXSourcesBuildPhase section */
186 |
187 | /* Begin PBXVariantGroup section */
188 | 8B3DC4C32422A2820067355E /* LaunchScreen.storyboard */ = {
189 | isa = PBXVariantGroup;
190 | children = (
191 | 8B3DC4C42422A2820067355E /* Base */,
192 | );
193 | name = LaunchScreen.storyboard;
194 | sourceTree = "";
195 | };
196 | /* End PBXVariantGroup section */
197 |
198 | /* Begin XCBuildConfiguration section */
199 | 8B3DC4C72422A2820067355E /* Debug */ = {
200 | isa = XCBuildConfiguration;
201 | buildSettings = {
202 | ALWAYS_SEARCH_USER_PATHS = NO;
203 | CLANG_ANALYZER_NONNULL = YES;
204 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
205 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
206 | CLANG_CXX_LIBRARY = "libc++";
207 | CLANG_ENABLE_MODULES = YES;
208 | CLANG_ENABLE_OBJC_ARC = YES;
209 | CLANG_ENABLE_OBJC_WEAK = YES;
210 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
211 | CLANG_WARN_BOOL_CONVERSION = YES;
212 | CLANG_WARN_COMMA = YES;
213 | CLANG_WARN_CONSTANT_CONVERSION = YES;
214 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
215 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
216 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
217 | CLANG_WARN_EMPTY_BODY = YES;
218 | CLANG_WARN_ENUM_CONVERSION = YES;
219 | CLANG_WARN_INFINITE_RECURSION = YES;
220 | CLANG_WARN_INT_CONVERSION = YES;
221 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
222 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
223 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
224 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
225 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
226 | CLANG_WARN_STRICT_PROTOTYPES = YES;
227 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
228 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
229 | CLANG_WARN_UNREACHABLE_CODE = YES;
230 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
231 | COPY_PHASE_STRIP = NO;
232 | DEBUG_INFORMATION_FORMAT = dwarf;
233 | ENABLE_STRICT_OBJC_MSGSEND = YES;
234 | ENABLE_TESTABILITY = YES;
235 | GCC_C_LANGUAGE_STANDARD = gnu11;
236 | GCC_DYNAMIC_NO_PIC = NO;
237 | GCC_NO_COMMON_BLOCKS = YES;
238 | GCC_OPTIMIZATION_LEVEL = 0;
239 | GCC_PREPROCESSOR_DEFINITIONS = (
240 | "DEBUG=1",
241 | "$(inherited)",
242 | );
243 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
244 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
245 | GCC_WARN_UNDECLARED_SELECTOR = YES;
246 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
247 | GCC_WARN_UNUSED_FUNCTION = YES;
248 | GCC_WARN_UNUSED_VARIABLE = YES;
249 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
250 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
251 | MTL_FAST_MATH = YES;
252 | ONLY_ACTIVE_ARCH = YES;
253 | SDKROOT = iphoneos;
254 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
255 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
256 | };
257 | name = Debug;
258 | };
259 | 8B3DC4C82422A2820067355E /* Release */ = {
260 | isa = XCBuildConfiguration;
261 | buildSettings = {
262 | ALWAYS_SEARCH_USER_PATHS = NO;
263 | CLANG_ANALYZER_NONNULL = YES;
264 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
265 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
266 | CLANG_CXX_LIBRARY = "libc++";
267 | CLANG_ENABLE_MODULES = YES;
268 | CLANG_ENABLE_OBJC_ARC = YES;
269 | CLANG_ENABLE_OBJC_WEAK = YES;
270 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
271 | CLANG_WARN_BOOL_CONVERSION = YES;
272 | CLANG_WARN_COMMA = YES;
273 | CLANG_WARN_CONSTANT_CONVERSION = YES;
274 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
275 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
276 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
277 | CLANG_WARN_EMPTY_BODY = YES;
278 | CLANG_WARN_ENUM_CONVERSION = YES;
279 | CLANG_WARN_INFINITE_RECURSION = YES;
280 | CLANG_WARN_INT_CONVERSION = YES;
281 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
282 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
283 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
284 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
285 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
286 | CLANG_WARN_STRICT_PROTOTYPES = YES;
287 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
288 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
289 | CLANG_WARN_UNREACHABLE_CODE = YES;
290 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
291 | COPY_PHASE_STRIP = NO;
292 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
293 | ENABLE_NS_ASSERTIONS = NO;
294 | ENABLE_STRICT_OBJC_MSGSEND = YES;
295 | GCC_C_LANGUAGE_STANDARD = gnu11;
296 | GCC_NO_COMMON_BLOCKS = YES;
297 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
298 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
299 | GCC_WARN_UNDECLARED_SELECTOR = YES;
300 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
301 | GCC_WARN_UNUSED_FUNCTION = YES;
302 | GCC_WARN_UNUSED_VARIABLE = YES;
303 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
304 | MTL_ENABLE_DEBUG_INFO = NO;
305 | MTL_FAST_MATH = YES;
306 | SDKROOT = iphoneos;
307 | SWIFT_COMPILATION_MODE = wholemodule;
308 | SWIFT_OPTIMIZATION_LEVEL = "-O";
309 | VALIDATE_PRODUCT = YES;
310 | };
311 | name = Release;
312 | };
313 | 8B3DC4CA2422A2820067355E /* Debug */ = {
314 | isa = XCBuildConfiguration;
315 | buildSettings = {
316 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
317 | CODE_SIGN_ENTITLEMENTS = MusicPlayer/MusicPlayer.entitlements;
318 | CODE_SIGN_STYLE = Automatic;
319 | DEVELOPMENT_ASSET_PATHS = "\"MusicPlayer/Preview Content\"";
320 | DEVELOPMENT_TEAM = XJYMG76226;
321 | ENABLE_PREVIEWS = YES;
322 | INFOPLIST_FILE = MusicPlayer/Info.plist;
323 | LD_RUNPATH_SEARCH_PATHS = (
324 | "$(inherited)",
325 | "@executable_path/Frameworks",
326 | );
327 | PRODUCT_BUNDLE_IDENTIFIER = com.appcoda.MusicPlayer;
328 | PRODUCT_NAME = "$(TARGET_NAME)";
329 | SUPPORTS_MACCATALYST = YES;
330 | SWIFT_VERSION = 5.0;
331 | TARGETED_DEVICE_FAMILY = "1,2";
332 | };
333 | name = Debug;
334 | };
335 | 8B3DC4CB2422A2820067355E /* Release */ = {
336 | isa = XCBuildConfiguration;
337 | buildSettings = {
338 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
339 | CODE_SIGN_ENTITLEMENTS = MusicPlayer/MusicPlayer.entitlements;
340 | CODE_SIGN_STYLE = Automatic;
341 | DEVELOPMENT_ASSET_PATHS = "\"MusicPlayer/Preview Content\"";
342 | DEVELOPMENT_TEAM = XJYMG76226;
343 | ENABLE_PREVIEWS = YES;
344 | INFOPLIST_FILE = MusicPlayer/Info.plist;
345 | LD_RUNPATH_SEARCH_PATHS = (
346 | "$(inherited)",
347 | "@executable_path/Frameworks",
348 | );
349 | PRODUCT_BUNDLE_IDENTIFIER = com.appcoda.MusicPlayer;
350 | PRODUCT_NAME = "$(TARGET_NAME)";
351 | SUPPORTS_MACCATALYST = YES;
352 | SWIFT_VERSION = 5.0;
353 | TARGETED_DEVICE_FAMILY = "1,2";
354 | };
355 | name = Release;
356 | };
357 | /* End XCBuildConfiguration section */
358 |
359 | /* Begin XCConfigurationList section */
360 | 8B3DC4B02422A2810067355E /* Build configuration list for PBXProject "MusicPlayer" */ = {
361 | isa = XCConfigurationList;
362 | buildConfigurations = (
363 | 8B3DC4C72422A2820067355E /* Debug */,
364 | 8B3DC4C82422A2820067355E /* Release */,
365 | );
366 | defaultConfigurationIsVisible = 0;
367 | defaultConfigurationName = Release;
368 | };
369 | 8B3DC4C92422A2820067355E /* Build configuration list for PBXNativeTarget "MusicPlayer" */ = {
370 | isa = XCConfigurationList;
371 | buildConfigurations = (
372 | 8B3DC4CA2422A2820067355E /* Debug */,
373 | 8B3DC4CB2422A2820067355E /* Release */,
374 | );
375 | defaultConfigurationIsVisible = 0;
376 | defaultConfigurationName = Release;
377 | };
378 | /* End XCConfigurationList section */
379 |
380 | /* Begin XCRemoteSwiftPackageReference section */
381 | 8B0ED901248D968B0055C5EB /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */ = {
382 | isa = XCRemoteSwiftPackageReference;
383 | repositoryURL = "https://github.com/SDWebImage/SDWebImageSwiftUI";
384 | requirement = {
385 | kind = upToNextMajorVersion;
386 | minimumVersion = 1.5.0;
387 | };
388 | };
389 | /* End XCRemoteSwiftPackageReference section */
390 |
391 | /* Begin XCSwiftPackageProductDependency section */
392 | 8B0ED902248D968B0055C5EB /* SDWebImageSwiftUI */ = {
393 | isa = XCSwiftPackageProductDependency;
394 | package = 8B0ED901248D968B0055C5EB /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
395 | productName = SDWebImageSwiftUI;
396 | };
397 | /* End XCSwiftPackageProductDependency section */
398 | };
399 | rootObject = 8B3DC4AD2422A2810067355E /* Project object */;
400 | }
401 |
--------------------------------------------------------------------------------
/MusicPlayer/SwiftyJSON.swift:
--------------------------------------------------------------------------------
1 | // SwiftyJSON.swift
2 | //
3 | // Copyright (c) 2014 - 2017 Ruoyu Fu, Pinglin Tang
4 | //
5 | // Permission is hereby granted, free of charge, to any person obtaining a copy
6 | // of this software and associated documentation files (the "Software"), to deal
7 | // in the Software without restriction, including without limitation the rights
8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | // copies of the Software, and to permit persons to whom the Software is
10 | // furnished to do so, subject to the following conditions:
11 | //
12 | // The above copyright notice and this permission notice shall be included in
13 | // all copies or substantial portions of the Software.
14 | //
15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | // THE SOFTWARE.
22 |
23 | import Foundation
24 |
25 | public enum SwiftyJSONError: Int, Swift.Error {
26 | case unsupportedType = 999
27 | case indexOutOfBounds = 900
28 | case elementTooDeep = 902
29 | case wrongType = 901
30 | case notExist = 500
31 | case invalidJSON = 490
32 | }
33 |
34 | extension SwiftyJSONError: CustomNSError {
35 |
36 | /// return the error domain of SwiftyJSONError
37 | public static var errorDomain: String { return "com.swiftyjson.SwiftyJSON" }
38 |
39 | /// return the error code of SwiftyJSONError
40 | public var errorCode: Int { return self.rawValue }
41 |
42 | /// return the userInfo of SwiftyJSONError
43 | public var errorUserInfo: [String: Any] {
44 | switch self {
45 | case .unsupportedType:
46 | return [NSLocalizedDescriptionKey: "It is an unsupported type."]
47 | case .indexOutOfBounds:
48 | return [NSLocalizedDescriptionKey: "Array Index is out of bounds."]
49 | case .wrongType:
50 | return [NSLocalizedDescriptionKey: "Couldn't merge, because the JSONs differ in type on top level."]
51 | case .notExist:
52 | return [NSLocalizedDescriptionKey: "Dictionary key does not exist."]
53 | case .invalidJSON:
54 | return [NSLocalizedDescriptionKey: "JSON is invalid."]
55 | case .elementTooDeep:
56 | return [NSLocalizedDescriptionKey: "Element too deep. Increase maxObjectDepth and make sure there is no reference loop."]
57 | }
58 | }
59 | }
60 |
61 | // MARK: - JSON Type
62 | /**
63 | JSON's type definitions.
64 | See http://www.json.org
65 | */
66 | public enum Type: Int {
67 | case number
68 | case string
69 | case bool
70 | case array
71 | case dictionary
72 | case null
73 | case unknown
74 | }
75 |
76 | // MARK: - JSON Base
77 | public struct JSON {
78 |
79 | /**
80 | Creates a JSON using the data.
81 |
82 | - parameter data: The NSData used to convert to json.Top level object in data is an NSArray or NSDictionary
83 | - parameter opt: The JSON serialization reading options. `[]` by default.
84 |
85 | - returns: The created JSON
86 | */
87 | public init(data: Data, options opt: JSONSerialization.ReadingOptions = []) throws {
88 | let object: Any = try JSONSerialization.jsonObject(with: data, options: opt)
89 | self.init(jsonObject: object)
90 | }
91 |
92 | /**
93 | Creates a JSON object
94 | - note: this does not parse a `String` into JSON, instead use `init(parseJSON: String)`
95 |
96 | - parameter object: the object
97 | - returns: the created JSON object
98 | */
99 | public init(_ object: Any) {
100 | switch object {
101 | case let object as Data:
102 | do {
103 | try self.init(data: object)
104 | } catch {
105 | self.init(jsonObject: NSNull())
106 | }
107 | default:
108 | self.init(jsonObject: object)
109 | }
110 | }
111 |
112 | /**
113 | Parses the JSON string into a JSON object
114 |
115 | - parameter json: the JSON string
116 |
117 | - returns: the created JSON object
118 | */
119 | public init(parseJSON jsonString: String) {
120 | if let data = jsonString.data(using: .utf8) {
121 | self.init(data)
122 | } else {
123 | self.init(NSNull())
124 | }
125 | }
126 |
127 | /**
128 | Creates a JSON using the object.
129 |
130 | - parameter jsonObject: The object must have the following properties: All objects are NSString/String, NSNumber/Int/Float/Double/Bool, NSArray/Array, NSDictionary/Dictionary, or NSNull; All dictionary keys are NSStrings/String; NSNumbers are not NaN or infinity.
131 |
132 | - returns: The created JSON
133 | */
134 | fileprivate init(jsonObject: Any) {
135 | object = jsonObject
136 | }
137 |
138 | /**
139 | Merges another JSON into this JSON, whereas primitive values which are not present in this JSON are getting added,
140 | present values getting overwritten, array values getting appended and nested JSONs getting merged the same way.
141 |
142 | - parameter other: The JSON which gets merged into this JSON
143 |
144 | - throws `ErrorWrongType` if the other JSONs differs in type on the top level.
145 | */
146 | public mutating func merge(with other: JSON) throws {
147 | try self.merge(with: other, typecheck: true)
148 | }
149 |
150 | /**
151 | Merges another JSON into this JSON and returns a new JSON, whereas primitive values which are not present in this JSON are getting added,
152 | present values getting overwritten, array values getting appended and nested JSONS getting merged the same way.
153 |
154 | - parameter other: The JSON which gets merged into this JSON
155 |
156 | - throws `ErrorWrongType` if the other JSONs differs in type on the top level.
157 |
158 | - returns: New merged JSON
159 | */
160 | public func merged(with other: JSON) throws -> JSON {
161 | var merged = self
162 | try merged.merge(with: other, typecheck: true)
163 | return merged
164 | }
165 |
166 | /**
167 | Private woker function which does the actual merging
168 | Typecheck is set to true for the first recursion level to prevent total override of the source JSON
169 | */
170 | fileprivate mutating func merge(with other: JSON, typecheck: Bool) throws {
171 | if type == other.type {
172 | switch type {
173 | case .dictionary:
174 | for (key, _) in other {
175 | try self[key].merge(with: other[key], typecheck: false)
176 | }
177 | case .array:
178 | self = JSON(arrayValue + other.arrayValue)
179 | default:
180 | self = other
181 | }
182 | } else {
183 | if typecheck {
184 | throw SwiftyJSONError.wrongType
185 | } else {
186 | self = other
187 | }
188 | }
189 | }
190 |
191 | /// Private object
192 | fileprivate var rawArray: [Any] = []
193 | fileprivate var rawDictionary: [String: Any] = [:]
194 | fileprivate var rawString: String = ""
195 | fileprivate var rawNumber: NSNumber = 0
196 | fileprivate var rawNull: NSNull = NSNull()
197 | fileprivate var rawBool: Bool = false
198 |
199 | /// JSON type, fileprivate setter
200 | public fileprivate(set) var type: Type = .null
201 |
202 | /// Error in JSON, fileprivate setter
203 | public fileprivate(set) var error: SwiftyJSONError?
204 |
205 | /// Object in JSON
206 | public var object: Any {
207 | get {
208 | switch type {
209 | case .array: return rawArray
210 | case .dictionary: return rawDictionary
211 | case .string: return rawString
212 | case .number: return rawNumber
213 | case .bool: return rawBool
214 | default: return rawNull
215 | }
216 | }
217 | set {
218 | error = nil
219 | switch unwrap(newValue) {
220 | case let number as NSNumber:
221 | if number.isBool {
222 | type = .bool
223 | rawBool = number.boolValue
224 | } else {
225 | type = .number
226 | rawNumber = number
227 | }
228 | case let string as String:
229 | type = .string
230 | rawString = string
231 | case _ as NSNull:
232 | type = .null
233 | case nil:
234 | type = .null
235 | case let array as [Any]:
236 | type = .array
237 | rawArray = array
238 | case let dictionary as [String: Any]:
239 | type = .dictionary
240 | rawDictionary = dictionary
241 | default:
242 | type = .unknown
243 | error = SwiftyJSONError.unsupportedType
244 | }
245 | }
246 | }
247 |
248 | /// The static null JSON
249 | @available(*, unavailable, renamed:"null")
250 | public static var nullJSON: JSON { return null }
251 | public static var null: JSON { return JSON(NSNull()) }
252 | }
253 |
254 | /// Private method to unwarp an object recursively
255 | private func unwrap(_ object: Any) -> Any {
256 | switch object {
257 | case let json as JSON:
258 | return unwrap(json.object)
259 | case let array as [Any]:
260 | return array.map(unwrap)
261 | case let dictionary as [String: Any]:
262 | var d = dictionary
263 | dictionary.forEach { pair in
264 | d[pair.key] = unwrap(pair.value)
265 | }
266 | return d
267 | default:
268 | return object
269 | }
270 | }
271 |
272 | public enum Index: Comparable {
273 | case array(Int)
274 | case dictionary(DictionaryIndex)
275 | case null
276 |
277 | static public func == (lhs: Index, rhs: Index) -> Bool {
278 | switch (lhs, rhs) {
279 | case (.array(let left), .array(let right)): return left == right
280 | case (.dictionary(let left), .dictionary(let right)): return left == right
281 | case (.null, .null): return true
282 | default: return false
283 | }
284 | }
285 |
286 | static public func < (lhs: Index, rhs: Index) -> Bool {
287 | switch (lhs, rhs) {
288 | case (.array(let left), .array(let right)): return left < right
289 | case (.dictionary(let left), .dictionary(let right)): return left < right
290 | default: return false
291 | }
292 | }
293 | }
294 |
295 | public typealias JSONIndex = Index
296 | public typealias JSONRawIndex = Index
297 |
298 | extension JSON: Swift.Collection {
299 |
300 | public typealias Index = JSONRawIndex
301 |
302 | public var startIndex: Index {
303 | switch type {
304 | case .array: return .array(rawArray.startIndex)
305 | case .dictionary: return .dictionary(rawDictionary.startIndex)
306 | default: return .null
307 | }
308 | }
309 |
310 | public var endIndex: Index {
311 | switch type {
312 | case .array: return .array(rawArray.endIndex)
313 | case .dictionary: return .dictionary(rawDictionary.endIndex)
314 | default: return .null
315 | }
316 | }
317 |
318 | public func index(after i: Index) -> Index {
319 | switch i {
320 | case .array(let idx): return .array(rawArray.index(after: idx))
321 | case .dictionary(let idx): return .dictionary(rawDictionary.index(after: idx))
322 | default: return .null
323 | }
324 | }
325 |
326 | public subscript (position: Index) -> (String, JSON) {
327 | switch position {
328 | case .array(let idx): return (String(idx), JSON(rawArray[idx]))
329 | case .dictionary(let idx): return (rawDictionary[idx].key, JSON(rawDictionary[idx].value))
330 | default: return ("", JSON.null)
331 | }
332 | }
333 | }
334 |
335 | // MARK: - Subscript
336 | /**
337 | * To mark both String and Int can be used in subscript.
338 | */
339 | public enum JSONKey {
340 | case index(Int)
341 | case key(String)
342 | }
343 |
344 | public protocol JSONSubscriptType {
345 | var jsonKey: JSONKey { get }
346 | }
347 |
348 | extension Int: JSONSubscriptType {
349 | public var jsonKey: JSONKey {
350 | return JSONKey.index(self)
351 | }
352 | }
353 |
354 | extension String: JSONSubscriptType {
355 | public var jsonKey: JSONKey {
356 | return JSONKey.key(self)
357 | }
358 | }
359 |
360 | extension JSON {
361 |
362 | /// If `type` is `.array`, return json whose object is `array[index]`, otherwise return null json with error.
363 | fileprivate subscript(index index: Int) -> JSON {
364 | get {
365 | if type != .array {
366 | var r = JSON.null
367 | r.error = self.error ?? SwiftyJSONError.wrongType
368 | return r
369 | } else if rawArray.indices.contains(index) {
370 | return JSON(rawArray[index])
371 | } else {
372 | var r = JSON.null
373 | r.error = SwiftyJSONError.indexOutOfBounds
374 | return r
375 | }
376 | }
377 | set {
378 | if type == .array &&
379 | rawArray.indices.contains(index) &&
380 | newValue.error == nil {
381 | rawArray[index] = newValue.object
382 | }
383 | }
384 | }
385 |
386 | /// If `type` is `.dictionary`, return json whose object is `dictionary[key]` , otherwise return null json with error.
387 | fileprivate subscript(key key: String) -> JSON {
388 | get {
389 | var r = JSON.null
390 | if type == .dictionary {
391 | if let o = rawDictionary[key] {
392 | r = JSON(o)
393 | } else {
394 | r.error = SwiftyJSONError.notExist
395 | }
396 | } else {
397 | r.error = self.error ?? SwiftyJSONError.wrongType
398 | }
399 | return r
400 | }
401 | set {
402 | if type == .dictionary && newValue.error == nil {
403 | rawDictionary[key] = newValue.object
404 | }
405 | }
406 | }
407 |
408 | /// If `sub` is `Int`, return `subscript(index:)`; If `sub` is `String`, return `subscript(key:)`.
409 | fileprivate subscript(sub sub: JSONSubscriptType) -> JSON {
410 | get {
411 | switch sub.jsonKey {
412 | case .index(let index): return self[index: index]
413 | case .key(let key): return self[key: key]
414 | }
415 | }
416 | set {
417 | switch sub.jsonKey {
418 | case .index(let index): self[index: index] = newValue
419 | case .key(let key): self[key: key] = newValue
420 | }
421 | }
422 | }
423 |
424 | /**
425 | Find a json in the complex data structures by using array of Int and/or String as path.
426 |
427 | Example:
428 |
429 | ```
430 | let json = JSON[data]
431 | let path = [9,"list","person","name"]
432 | let name = json[path]
433 | ```
434 |
435 | The same as: let name = json[9]["list"]["person"]["name"]
436 |
437 | - parameter path: The target json's path.
438 |
439 | - returns: Return a json found by the path or a null json with error
440 | */
441 | public subscript(path: [JSONSubscriptType]) -> JSON {
442 | get {
443 | return path.reduce(self) { $0[sub: $1] }
444 | }
445 | set {
446 | switch path.count {
447 | case 0: return
448 | case 1: self[sub:path[0]].object = newValue.object
449 | default:
450 | var aPath = path
451 | aPath.remove(at: 0)
452 | var nextJSON = self[sub: path[0]]
453 | nextJSON[aPath] = newValue
454 | self[sub: path[0]] = nextJSON
455 | }
456 | }
457 | }
458 |
459 | /**
460 | Find a json in the complex data structures by using array of Int and/or String as path.
461 | - parameter path: The target json's path. Example:
462 | let name = json[9,"list","person","name"]
463 | The same as: let name = json[9]["list"]["person"]["name"]
464 | - returns: Return a json found by the path or a null json with error
465 | */
466 | public subscript(path: JSONSubscriptType...) -> JSON {
467 | get {
468 | return self[path]
469 | }
470 | set {
471 | self[path] = newValue
472 | }
473 | }
474 | }
475 |
476 | // MARK: - LiteralConvertible
477 | extension JSON: Swift.ExpressibleByStringLiteral {
478 |
479 | public init(stringLiteral value: StringLiteralType) {
480 | self.init(value)
481 | }
482 |
483 | public init(extendedGraphemeClusterLiteral value: StringLiteralType) {
484 | self.init(value)
485 | }
486 |
487 | public init(unicodeScalarLiteral value: StringLiteralType) {
488 | self.init(value)
489 | }
490 | }
491 |
492 | extension JSON: Swift.ExpressibleByIntegerLiteral {
493 |
494 | public init(integerLiteral value: IntegerLiteralType) {
495 | self.init(value)
496 | }
497 | }
498 |
499 | extension JSON: Swift.ExpressibleByBooleanLiteral {
500 |
501 | public init(booleanLiteral value: BooleanLiteralType) {
502 | self.init(value)
503 | }
504 | }
505 |
506 | extension JSON: Swift.ExpressibleByFloatLiteral {
507 |
508 | public init(floatLiteral value: FloatLiteralType) {
509 | self.init(value)
510 | }
511 | }
512 |
513 | extension JSON: Swift.ExpressibleByDictionaryLiteral {
514 | public init(dictionaryLiteral elements: (String, Any)...) {
515 | let dictionary = elements.reduce(into: [String: Any](), { $0[$1.0] = $1.1})
516 | self.init(dictionary)
517 | }
518 | }
519 |
520 | extension JSON: Swift.ExpressibleByArrayLiteral {
521 |
522 | public init(arrayLiteral elements: Any...) {
523 | self.init(elements)
524 | }
525 | }
526 |
527 | // MARK: - Raw
528 | extension JSON: Swift.RawRepresentable {
529 |
530 | public init?(rawValue: Any) {
531 | if JSON(rawValue).type == .unknown {
532 | return nil
533 | } else {
534 | self.init(rawValue)
535 | }
536 | }
537 |
538 | public var rawValue: Any {
539 | return object
540 | }
541 |
542 | public func rawData(options opt: JSONSerialization.WritingOptions = JSONSerialization.WritingOptions(rawValue: 0)) throws -> Data {
543 | guard JSONSerialization.isValidJSONObject(object) else {
544 | throw SwiftyJSONError.invalidJSON
545 | }
546 |
547 | return try JSONSerialization.data(withJSONObject: object, options: opt)
548 | }
549 |
550 | public func rawString(_ encoding: String.Encoding = .utf8, options opt: JSONSerialization.WritingOptions = .prettyPrinted) -> String? {
551 | do {
552 | return try _rawString(encoding, options: [.jsonSerialization: opt])
553 | } catch {
554 | print("Could not serialize object to JSON because:", error.localizedDescription)
555 | return nil
556 | }
557 | }
558 |
559 | public func rawString(_ options: [writingOptionsKeys: Any]) -> String? {
560 | let encoding = options[.encoding] as? String.Encoding ?? String.Encoding.utf8
561 | let maxObjectDepth = options[.maxObjextDepth] as? Int ?? 10
562 | do {
563 | return try _rawString(encoding, options: options, maxObjectDepth: maxObjectDepth)
564 | } catch {
565 | print("Could not serialize object to JSON because:", error.localizedDescription)
566 | return nil
567 | }
568 | }
569 |
570 | fileprivate func _rawString(_ encoding: String.Encoding = .utf8, options: [writingOptionsKeys: Any], maxObjectDepth: Int = 10) throws -> String? {
571 | guard maxObjectDepth > 0 else { throw SwiftyJSONError.invalidJSON }
572 | switch type {
573 | case .dictionary:
574 | do {
575 | if !(options[.castNilToNSNull] as? Bool ?? false) {
576 | let jsonOption = options[.jsonSerialization] as? JSONSerialization.WritingOptions ?? JSONSerialization.WritingOptions.prettyPrinted
577 | let data = try rawData(options: jsonOption)
578 | return String(data: data, encoding: encoding)
579 | }
580 |
581 | guard let dict = object as? [String: Any?] else {
582 | return nil
583 | }
584 | let body = try dict.keys.map { key throws -> String in
585 | guard let value = dict[key] else {
586 | return "\"\(key)\": null"
587 | }
588 | guard let unwrappedValue = value else {
589 | return "\"\(key)\": null"
590 | }
591 |
592 | let nestedValue = JSON(unwrappedValue)
593 | guard let nestedString = try nestedValue._rawString(encoding, options: options, maxObjectDepth: maxObjectDepth - 1) else {
594 | throw SwiftyJSONError.elementTooDeep
595 | }
596 | if nestedValue.type == .string {
597 | return "\"\(key)\": \"\(nestedString.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\""
598 | } else {
599 | return "\"\(key)\": \(nestedString)"
600 | }
601 | }
602 |
603 | return "{\(body.joined(separator: ","))}"
604 | } catch _ {
605 | return nil
606 | }
607 | case .array:
608 | do {
609 | if !(options[.castNilToNSNull] as? Bool ?? false) {
610 | let jsonOption = options[.jsonSerialization] as? JSONSerialization.WritingOptions ?? JSONSerialization.WritingOptions.prettyPrinted
611 | let data = try rawData(options: jsonOption)
612 | return String(data: data, encoding: encoding)
613 | }
614 |
615 | guard let array = object as? [Any?] else {
616 | return nil
617 | }
618 | let body = try array.map { value throws -> String in
619 | guard let unwrappedValue = value else {
620 | return "null"
621 | }
622 |
623 | let nestedValue = JSON(unwrappedValue)
624 | guard let nestedString = try nestedValue._rawString(encoding, options: options, maxObjectDepth: maxObjectDepth - 1) else {
625 | throw SwiftyJSONError.invalidJSON
626 | }
627 | if nestedValue.type == .string {
628 | return "\"\(nestedString.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\""))\""
629 | } else {
630 | return nestedString
631 | }
632 | }
633 |
634 | return "[\(body.joined(separator: ","))]"
635 | } catch _ {
636 | return nil
637 | }
638 | case .string: return rawString
639 | case .number: return rawNumber.stringValue
640 | case .bool: return rawBool.description
641 | case .null: return "null"
642 | default: return nil
643 | }
644 | }
645 | }
646 |
647 | // MARK: - Printable, DebugPrintable
648 | extension JSON: Swift.CustomStringConvertible, Swift.CustomDebugStringConvertible {
649 |
650 | public var description: String {
651 | return rawString(options: .prettyPrinted) ?? "unknown"
652 | }
653 |
654 | public var debugDescription: String {
655 | return description
656 | }
657 | }
658 |
659 | // MARK: - Array
660 | extension JSON {
661 |
662 | //Optional [JSON]
663 | public var array: [JSON]? {
664 | return type == .array ? rawArray.map { JSON($0) } : nil
665 | }
666 |
667 | //Non-optional [JSON]
668 | public var arrayValue: [JSON] {
669 | return self.array ?? []
670 | }
671 |
672 | //Optional [Any]
673 | public var arrayObject: [Any]? {
674 | get {
675 | switch type {
676 | case .array: return rawArray
677 | default: return nil
678 | }
679 | }
680 | set {
681 | self.object = newValue ?? NSNull()
682 | }
683 | }
684 | }
685 |
686 | // MARK: - Dictionary
687 | extension JSON {
688 |
689 | //Optional [String : JSON]
690 | public var dictionary: [String: JSON]? {
691 | if type == .dictionary {
692 | var d = [String: JSON](minimumCapacity: rawDictionary.count)
693 | rawDictionary.forEach { pair in
694 | d[pair.key] = JSON(pair.value)
695 | }
696 | return d
697 | } else {
698 | return nil
699 | }
700 | }
701 |
702 | //Non-optional [String : JSON]
703 | public var dictionaryValue: [String: JSON] {
704 | return dictionary ?? [:]
705 | }
706 |
707 | //Optional [String : Any]
708 | public var dictionaryObject: [String: Any]? {
709 | get {
710 | switch type {
711 | case .dictionary: return rawDictionary
712 | default: return nil
713 | }
714 | }
715 | set {
716 | object = newValue ?? NSNull()
717 | }
718 | }
719 | }
720 |
721 | // MARK: - Bool
722 | extension JSON { // : Swift.Bool
723 | //Optional bool
724 | public var bool: Bool? {
725 | get {
726 | switch type {
727 | case .bool: return rawBool
728 | default: return nil
729 | }
730 | }
731 | set {
732 | object = newValue ?? NSNull()
733 | }
734 | }
735 |
736 | //Non-optional bool
737 | public var boolValue: Bool {
738 | get {
739 | switch type {
740 | case .bool: return rawBool
741 | case .number: return rawNumber.boolValue
742 | case .string: return ["true", "y", "t", "yes", "1"].contains { rawString.caseInsensitiveCompare($0) == .orderedSame }
743 | default: return false
744 | }
745 | }
746 | set {
747 | object = newValue
748 | }
749 | }
750 | }
751 |
752 | // MARK: - String
753 | extension JSON {
754 |
755 | //Optional string
756 | public var string: String? {
757 | get {
758 | switch type {
759 | case .string: return object as? String
760 | default: return nil
761 | }
762 | }
763 | set {
764 | object = newValue ?? NSNull()
765 | }
766 | }
767 |
768 | //Non-optional string
769 | public var stringValue: String {
770 | get {
771 | switch type {
772 | case .string: return object as? String ?? ""
773 | case .number: return rawNumber.stringValue
774 | case .bool: return (object as? Bool).map { String($0) } ?? ""
775 | default: return ""
776 | }
777 | }
778 | set {
779 | object = newValue
780 | }
781 | }
782 | }
783 |
784 | // MARK: - Number
785 | extension JSON {
786 |
787 | //Optional number
788 | public var number: NSNumber? {
789 | get {
790 | switch type {
791 | case .number: return rawNumber
792 | case .bool: return NSNumber(value: rawBool ? 1 : 0)
793 | default: return nil
794 | }
795 | }
796 | set {
797 | object = newValue ?? NSNull()
798 | }
799 | }
800 |
801 | //Non-optional number
802 | public var numberValue: NSNumber {
803 | get {
804 | switch type {
805 | case .string:
806 | let decimal = NSDecimalNumber(string: object as? String)
807 | return decimal == .notANumber ? .zero : decimal
808 | case .number: return object as? NSNumber ?? NSNumber(value: 0)
809 | case .bool: return NSNumber(value: rawBool ? 1 : 0)
810 | default: return NSNumber(value: 0.0)
811 | }
812 | }
813 | set {
814 | object = newValue
815 | }
816 | }
817 | }
818 |
819 | // MARK: - Null
820 | extension JSON {
821 |
822 | public var null: NSNull? {
823 | set {
824 | object = NSNull()
825 | }
826 | get {
827 | switch type {
828 | case .null: return rawNull
829 | default: return nil
830 | }
831 | }
832 | }
833 | public func exists() -> Bool {
834 | if let errorValue = error, (400...1000).contains(errorValue.errorCode) {
835 | return false
836 | }
837 | return true
838 | }
839 | }
840 |
841 | // MARK: - URL
842 | extension JSON {
843 |
844 | //Optional URL
845 | public var url: URL? {
846 | get {
847 | switch type {
848 | case .string:
849 | // Check for existing percent escapes first to prevent double-escaping of % character
850 | if rawString.range(of: "%[0-9A-Fa-f]{2}", options: .regularExpression, range: nil, locale: nil) != nil {
851 | return Foundation.URL(string: rawString)
852 | } else if let encodedString_ = rawString.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) {
853 | // We have to use `Foundation.URL` otherwise it conflicts with the variable name.
854 | return Foundation.URL(string: encodedString_)
855 | } else {
856 | return nil
857 | }
858 | default:
859 | return nil
860 | }
861 | }
862 | set {
863 | object = newValue?.absoluteString ?? NSNull()
864 | }
865 | }
866 | }
867 |
868 | // MARK: - Int, Double, Float, Int8, Int16, Int32, Int64
869 | extension JSON {
870 |
871 | public var double: Double? {
872 | get {
873 | return number?.doubleValue
874 | }
875 | set {
876 | if let newValue = newValue {
877 | object = NSNumber(value: newValue)
878 | } else {
879 | object = NSNull()
880 | }
881 | }
882 | }
883 |
884 | public var doubleValue: Double {
885 | get {
886 | return numberValue.doubleValue
887 | }
888 | set {
889 | object = NSNumber(value: newValue)
890 | }
891 | }
892 |
893 | public var float: Float? {
894 | get {
895 | return number?.floatValue
896 | }
897 | set {
898 | if let newValue = newValue {
899 | object = NSNumber(value: newValue)
900 | } else {
901 | object = NSNull()
902 | }
903 | }
904 | }
905 |
906 | public var floatValue: Float {
907 | get {
908 | return numberValue.floatValue
909 | }
910 | set {
911 | object = NSNumber(value: newValue)
912 | }
913 | }
914 |
915 | public var int: Int? {
916 | get {
917 | return number?.intValue
918 | }
919 | set {
920 | if let newValue = newValue {
921 | object = NSNumber(value: newValue)
922 | } else {
923 | object = NSNull()
924 | }
925 | }
926 | }
927 |
928 | public var intValue: Int {
929 | get {
930 | return numberValue.intValue
931 | }
932 | set {
933 | object = NSNumber(value: newValue)
934 | }
935 | }
936 |
937 | public var uInt: UInt? {
938 | get {
939 | return number?.uintValue
940 | }
941 | set {
942 | if let newValue = newValue {
943 | object = NSNumber(value: newValue)
944 | } else {
945 | object = NSNull()
946 | }
947 | }
948 | }
949 |
950 | public var uIntValue: UInt {
951 | get {
952 | return numberValue.uintValue
953 | }
954 | set {
955 | object = NSNumber(value: newValue)
956 | }
957 | }
958 |
959 | public var int8: Int8? {
960 | get {
961 | return number?.int8Value
962 | }
963 | set {
964 | if let newValue = newValue {
965 | object = NSNumber(value: Int(newValue))
966 | } else {
967 | object = NSNull()
968 | }
969 | }
970 | }
971 |
972 | public var int8Value: Int8 {
973 | get {
974 | return numberValue.int8Value
975 | }
976 | set {
977 | object = NSNumber(value: Int(newValue))
978 | }
979 | }
980 |
981 | public var uInt8: UInt8? {
982 | get {
983 | return number?.uint8Value
984 | }
985 | set {
986 | if let newValue = newValue {
987 | object = NSNumber(value: newValue)
988 | } else {
989 | object = NSNull()
990 | }
991 | }
992 | }
993 |
994 | public var uInt8Value: UInt8 {
995 | get {
996 | return numberValue.uint8Value
997 | }
998 | set {
999 | object = NSNumber(value: newValue)
1000 | }
1001 | }
1002 |
1003 | public var int16: Int16? {
1004 | get {
1005 | return number?.int16Value
1006 | }
1007 | set {
1008 | if let newValue = newValue {
1009 | object = NSNumber(value: newValue)
1010 | } else {
1011 | object = NSNull()
1012 | }
1013 | }
1014 | }
1015 |
1016 | public var int16Value: Int16 {
1017 | get {
1018 | return numberValue.int16Value
1019 | }
1020 | set {
1021 | object = NSNumber(value: newValue)
1022 | }
1023 | }
1024 |
1025 | public var uInt16: UInt16? {
1026 | get {
1027 | return number?.uint16Value
1028 | }
1029 | set {
1030 | if let newValue = newValue {
1031 | object = NSNumber(value: newValue)
1032 | } else {
1033 | object = NSNull()
1034 | }
1035 | }
1036 | }
1037 |
1038 | public var uInt16Value: UInt16 {
1039 | get {
1040 | return numberValue.uint16Value
1041 | }
1042 | set {
1043 | object = NSNumber(value: newValue)
1044 | }
1045 | }
1046 |
1047 | public var int32: Int32? {
1048 | get {
1049 | return number?.int32Value
1050 | }
1051 | set {
1052 | if let newValue = newValue {
1053 | object = NSNumber(value: newValue)
1054 | } else {
1055 | object = NSNull()
1056 | }
1057 | }
1058 | }
1059 |
1060 | public var int32Value: Int32 {
1061 | get {
1062 | return numberValue.int32Value
1063 | }
1064 | set {
1065 | object = NSNumber(value: newValue)
1066 | }
1067 | }
1068 |
1069 | public var uInt32: UInt32? {
1070 | get {
1071 | return number?.uint32Value
1072 | }
1073 | set {
1074 | if let newValue = newValue {
1075 | object = NSNumber(value: newValue)
1076 | } else {
1077 | object = NSNull()
1078 | }
1079 | }
1080 | }
1081 |
1082 | public var uInt32Value: UInt32 {
1083 | get {
1084 | return numberValue.uint32Value
1085 | }
1086 | set {
1087 | object = NSNumber(value: newValue)
1088 | }
1089 | }
1090 |
1091 | public var int64: Int64? {
1092 | get {
1093 | return number?.int64Value
1094 | }
1095 | set {
1096 | if let newValue = newValue {
1097 | object = NSNumber(value: newValue)
1098 | } else {
1099 | object = NSNull()
1100 | }
1101 | }
1102 | }
1103 |
1104 | public var int64Value: Int64 {
1105 | get {
1106 | return numberValue.int64Value
1107 | }
1108 | set {
1109 | object = NSNumber(value: newValue)
1110 | }
1111 | }
1112 |
1113 | public var uInt64: UInt64? {
1114 | get {
1115 | return number?.uint64Value
1116 | }
1117 | set {
1118 | if let newValue = newValue {
1119 | object = NSNumber(value: newValue)
1120 | } else {
1121 | object = NSNull()
1122 | }
1123 | }
1124 | }
1125 |
1126 | public var uInt64Value: UInt64 {
1127 | get {
1128 | return numberValue.uint64Value
1129 | }
1130 | set {
1131 | object = NSNumber(value: newValue)
1132 | }
1133 | }
1134 | }
1135 |
1136 | // MARK: - Comparable
1137 | extension JSON: Swift.Comparable {}
1138 |
1139 | public func == (lhs: JSON, rhs: JSON) -> Bool {
1140 |
1141 | switch (lhs.type, rhs.type) {
1142 | case (.number, .number): return lhs.rawNumber == rhs.rawNumber
1143 | case (.string, .string): return lhs.rawString == rhs.rawString
1144 | case (.bool, .bool): return lhs.rawBool == rhs.rawBool
1145 | case (.array, .array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray
1146 | case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary
1147 | case (.null, .null): return true
1148 | default: return false
1149 | }
1150 | }
1151 |
1152 | public func <= (lhs: JSON, rhs: JSON) -> Bool {
1153 |
1154 | switch (lhs.type, rhs.type) {
1155 | case (.number, .number): return lhs.rawNumber <= rhs.rawNumber
1156 | case (.string, .string): return lhs.rawString <= rhs.rawString
1157 | case (.bool, .bool): return lhs.rawBool == rhs.rawBool
1158 | case (.array, .array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray
1159 | case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary
1160 | case (.null, .null): return true
1161 | default: return false
1162 | }
1163 | }
1164 |
1165 | public func >= (lhs: JSON, rhs: JSON) -> Bool {
1166 |
1167 | switch (lhs.type, rhs.type) {
1168 | case (.number, .number): return lhs.rawNumber >= rhs.rawNumber
1169 | case (.string, .string): return lhs.rawString >= rhs.rawString
1170 | case (.bool, .bool): return lhs.rawBool == rhs.rawBool
1171 | case (.array, .array): return lhs.rawArray as NSArray == rhs.rawArray as NSArray
1172 | case (.dictionary, .dictionary): return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary
1173 | case (.null, .null): return true
1174 | default: return false
1175 | }
1176 | }
1177 |
1178 | public func > (lhs: JSON, rhs: JSON) -> Bool {
1179 |
1180 | switch (lhs.type, rhs.type) {
1181 | case (.number, .number): return lhs.rawNumber > rhs.rawNumber
1182 | case (.string, .string): return lhs.rawString > rhs.rawString
1183 | default: return false
1184 | }
1185 | }
1186 |
1187 | public func < (lhs: JSON, rhs: JSON) -> Bool {
1188 |
1189 | switch (lhs.type, rhs.type) {
1190 | case (.number, .number): return lhs.rawNumber < rhs.rawNumber
1191 | case (.string, .string): return lhs.rawString < rhs.rawString
1192 | default: return false
1193 | }
1194 | }
1195 |
1196 | private let trueNumber = NSNumber(value: true)
1197 | private let falseNumber = NSNumber(value: false)
1198 | private let trueObjCType = String(cString: trueNumber.objCType)
1199 | private let falseObjCType = String(cString: falseNumber.objCType)
1200 |
1201 | // MARK: - NSNumber: Comparable
1202 | extension NSNumber {
1203 | fileprivate var isBool: Bool {
1204 | let objCType = String(cString: self.objCType)
1205 | if (self.compare(trueNumber) == .orderedSame && objCType == trueObjCType) || (self.compare(falseNumber) == .orderedSame && objCType == falseObjCType) {
1206 | return true
1207 | } else {
1208 | return false
1209 | }
1210 | }
1211 | }
1212 |
1213 | func == (lhs: NSNumber, rhs: NSNumber) -> Bool {
1214 | switch (lhs.isBool, rhs.isBool) {
1215 | case (false, true): return false
1216 | case (true, false): return false
1217 | default: return lhs.compare(rhs) == .orderedSame
1218 | }
1219 | }
1220 |
1221 | func != (lhs: NSNumber, rhs: NSNumber) -> Bool {
1222 | return !(lhs == rhs)
1223 | }
1224 |
1225 | func < (lhs: NSNumber, rhs: NSNumber) -> Bool {
1226 |
1227 | switch (lhs.isBool, rhs.isBool) {
1228 | case (false, true): return false
1229 | case (true, false): return false
1230 | default: return lhs.compare(rhs) == .orderedAscending
1231 | }
1232 | }
1233 |
1234 | func > (lhs: NSNumber, rhs: NSNumber) -> Bool {
1235 |
1236 | switch (lhs.isBool, rhs.isBool) {
1237 | case (false, true): return false
1238 | case (true, false): return false
1239 | default: return lhs.compare(rhs) == ComparisonResult.orderedDescending
1240 | }
1241 | }
1242 |
1243 | func <= (lhs: NSNumber, rhs: NSNumber) -> Bool {
1244 |
1245 | switch (lhs.isBool, rhs.isBool) {
1246 | case (false, true): return false
1247 | case (true, false): return false
1248 | default: return lhs.compare(rhs) != .orderedDescending
1249 | }
1250 | }
1251 |
1252 | func >= (lhs: NSNumber, rhs: NSNumber) -> Bool {
1253 |
1254 | switch (lhs.isBool, rhs.isBool) {
1255 | case (false, true): return false
1256 | case (true, false): return false
1257 | default: return lhs.compare(rhs) != .orderedAscending
1258 | }
1259 | }
1260 |
1261 | public enum writingOptionsKeys {
1262 | case jsonSerialization
1263 | case castNilToNSNull
1264 | case maxObjextDepth
1265 | case encoding
1266 | }
1267 |
1268 | // MARK: - JSON: Codable
1269 | extension JSON: Codable {
1270 | private static var codableTypes: [Codable.Type] {
1271 | return [
1272 | Bool.self,
1273 | Int.self,
1274 | Int8.self,
1275 | Int16.self,
1276 | Int32.self,
1277 | Int64.self,
1278 | UInt.self,
1279 | UInt8.self,
1280 | UInt16.self,
1281 | UInt32.self,
1282 | UInt64.self,
1283 | Double.self,
1284 | String.self,
1285 | [JSON].self,
1286 | [String: JSON].self
1287 | ]
1288 | }
1289 | public init(from decoder: Decoder) throws {
1290 | var object: Any?
1291 |
1292 | if let container = try? decoder.singleValueContainer(), !container.decodeNil() {
1293 | for type in JSON.codableTypes {
1294 | if object != nil {
1295 | break
1296 | }
1297 | // try to decode value
1298 | switch type {
1299 | case let boolType as Bool.Type:
1300 | object = try? container.decode(boolType)
1301 | case let intType as Int.Type:
1302 | object = try? container.decode(intType)
1303 | case let int8Type as Int8.Type:
1304 | object = try? container.decode(int8Type)
1305 | case let int32Type as Int32.Type:
1306 | object = try? container.decode(int32Type)
1307 | case let int64Type as Int64.Type:
1308 | object = try? container.decode(int64Type)
1309 | case let uintType as UInt.Type:
1310 | object = try? container.decode(uintType)
1311 | case let uint8Type as UInt8.Type:
1312 | object = try? container.decode(uint8Type)
1313 | case let uint16Type as UInt16.Type:
1314 | object = try? container.decode(uint16Type)
1315 | case let uint32Type as UInt32.Type:
1316 | object = try? container.decode(uint32Type)
1317 | case let uint64Type as UInt64.Type:
1318 | object = try? container.decode(uint64Type)
1319 | case let doubleType as Double.Type:
1320 | object = try? container.decode(doubleType)
1321 | case let stringType as String.Type:
1322 | object = try? container.decode(stringType)
1323 | case let jsonValueArrayType as [JSON].Type:
1324 | object = try? container.decode(jsonValueArrayType)
1325 | case let jsonValueDictType as [String: JSON].Type:
1326 | object = try? container.decode(jsonValueDictType)
1327 | default:
1328 | break
1329 | }
1330 | }
1331 | }
1332 | self.init(object ?? NSNull())
1333 | }
1334 | public func encode(to encoder: Encoder) throws {
1335 | var container = encoder.singleValueContainer()
1336 | if object is NSNull {
1337 | try container.encodeNil()
1338 | return
1339 | }
1340 | switch object {
1341 | case let intValue as Int:
1342 | try container.encode(intValue)
1343 | case let int8Value as Int8:
1344 | try container.encode(int8Value)
1345 | case let int32Value as Int32:
1346 | try container.encode(int32Value)
1347 | case let int64Value as Int64:
1348 | try container.encode(int64Value)
1349 | case let uintValue as UInt:
1350 | try container.encode(uintValue)
1351 | case let uint8Value as UInt8:
1352 | try container.encode(uint8Value)
1353 | case let uint16Value as UInt16:
1354 | try container.encode(uint16Value)
1355 | case let uint32Value as UInt32:
1356 | try container.encode(uint32Value)
1357 | case let uint64Value as UInt64:
1358 | try container.encode(uint64Value)
1359 | case let doubleValue as Double:
1360 | try container.encode(doubleValue)
1361 | case let boolValue as Bool:
1362 | try container.encode(boolValue)
1363 | case let stringValue as String:
1364 | try container.encode(stringValue)
1365 | case is [Any]:
1366 | let jsonValueArray = array ?? []
1367 | try container.encode(jsonValueArray)
1368 | case is [String: Any]:
1369 | let jsonValueDictValue = dictionary ?? [:]
1370 | try container.encode(jsonValueDictValue)
1371 | default:
1372 | break
1373 | }
1374 | }
1375 | }
1376 |
--------------------------------------------------------------------------------