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