An Elegant Spotify Web API Library Written in Swift for iOS and macOS

Overview

Spartan

CI Status Version Language: Swift License Platform

Written in Swift 4.2

Spartan is a lightweight, elegant, and easy to use Spotify Web API wrapper library for iOS and macOS written in Swift 3. Under the hood, Spartan makes request to the Spotify Web API. Those requests are then turned into clean, friendly, and easy to use objects.

What this library allows you to do:

  • βœ… Make any request that the Spotify Web API allows.

What this library doesn't do:

  • ❌ Authorization flows that help provide you with an authorization token. The Spotify iOS SDK can help assist you with that.

Requirements

  • iOS 9.0+ / macOS 10.11+
  • xCode 9.0+

Installation

CocoaPods

To integrate Spartan into your xCode project using CocoaPods, specify it in your Podfile:

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'   # If you targeting iOS
platform :osx, '10.11' # If you targeting macOS
use_frameworks!

pod 'Spartan'

Then, run the following command:

$ pod install

This will download any library dependencies you do not already have in your project.

Usage

Spartan Properties

Authorization Token

public static var authorizationToken: String?

This is the token that is included with each request that requires OAuth authentication. If the request you make requires OAuth and this in nil, invalid, or expired, the request will fail. This can be left nil if you do not plan on making any requests that require OAuth authentication.

Each time your token is refreshed, simply update it:

Spartan.authorizationToken = newToken

Logging

public static var loggingEnabled: Bool = true

When enabled, before each request starts and after each request finishes, helpful statements will be printed to the console.

A successful request will look something like this:

πŸ”΅ [SpartanRequestLogger] GET https://api.spotify.com/v1/me
βšͺ️ [SpartanRequestLogger] GET https://api.spotify.com/v1/me (200 OK) 0.140969038009644 seconds

While a failed/invalid request will look something like this:

πŸ”΅ [SpartanRequestLogger] GET https://api.spotify.com/v1/me
πŸ”΄ [SpartanRequestLogger] GET https://api.spotify.com/v1/me (401 Unauthorized) 0.81086003780365 seconds

This can be enabled/disabled at any time anywhere in your code simply by:

Spartan.loggingEnabled = true/false

Core

For more information on each request, please click the link associated with it. Spotify provides excellent documentation for each api request that they support. Spartan supports almost all query parameter fields that Spotify allows.

PagingObject

Quite a few requests return a PagingObject. This is an offset based paging object that is a container for a set of objects. It contains a key called items whose value is an array of the requested objects. It can be used to get the previous set of items or the next set of items for a future call.

For example:

_ = Spartan.search(query: "Five for Fighting", type: .track, success: { (pagingObject: PagingObject<Track>) in
	self.pagingObject = pagingObject
}, failure: { (error) in
	print(error)
})

Then later, if you need to load more data (scrolled to bottom of UITableView/UICollectionView, etc...) more data can be loaded/reloaded with these two methods:

Get Next

if pagingObject.canMakeNextRequest {

	pagingObject.getNext(success: { (pagingObject) in
   		// Update the paging object
   		self.pagingObject = pagingObject            
	}, failure: { (error) in
		print(error)
	})
}

Get Previous

if pagingObject.canMakePreviousRequest {

	pagingObject.getPrevious(success: { (pagingObject) in
   		// Update the paging object
   		self.pagingObject = pagingObject            
	}, failure: { (error) in
		print(error)
	})
}

Requests

Get an Album

_ = Spartan.getAlbum(id: albumId, market: .us, success: { (album) in
	// Do something with the album    
}, failure: { (error) in
	print(error)      
})

Get Several Albums

_ = Spartan.getAlbums(ids: albumIds, market: .us, success: { (albums) in
	// Do something with the albums
}, failure: { (error) in
	print(error)
})

Get an Album's Tracks

_ = Spartan.getTracks(albumId: albumId, limit: 20, offset: 0, market: .us, success: { (pagingObject) in
	// Get the tracks via pagingObject.items
}, failure: { (error) in
	print(error)
})

Get an Artist

_ = Spartan.getArtist(id: artistId, success: { (artist) in
	// Do something with the artist
}, failure: { (error) in
	print(error)
})

Get Several Artists

_ = Spartan.getArtists(ids: artistIds, success: { (artists) in
	// Do something with the artists
}, failure: { (error) in
	print(error)
})

Get a Track

_ = Spartan.getTrack(id: track, market: .us, success: { (track) in
	// Do something with the track
}, failure: { (error) in
	print(error)
})

Get Several Tracks

_ = Spartan.getTracks(ids: trackIds, market: .us, success: { (tracks) in
	// Do something with the tracks
}, failure: { (error) in
	print(error)
})

Get an Artist's Albums

_ = Spartan.getArtistAlbums(artistId: artistId, limit: 20, offset: 0, albumTypes: [.album, .single, .appearsOn, .compilation], market: .us, success: { (pagingObject) in
	// Get the albums via pagingObject.items
}, failure: { (error) in
	print(error)
})

Get an Artist's Top Tracks

 _ = Spartan.getArtistsTopTracks(artistId: artistId, country: .us, success: { (tracks) in
	// Do something with the tracks
}, failure: { (error) in
	print(error)
})

Get an Artist's Related Artists

_ = Spartan.getArtistsRelatedArtists(artistId: artistId, success: { (artists) in
	// Do something with the artists
}, failure: { (error) in
	print(error)
})

Search for Albums

_ = Spartan.search(query: query, type: .album, success: { (pagingObject: PagingObject<SimplifiedAlbum>) in
	// Get the albums via pagingObject.items     
}, failure: { (error) in
	print(error)
})

Search for Artists

_ = Spartan.search(query: query, type: .artist, success: { (pagingObject: PagingObject<SimplifiedArtist>) in
	// Get the artists via pagingObject.items     
}, failure: { (error) in
	print(error)
})

Search for Playlists

_ = Spartan.search(query: query, type: .playlist, success: { (pagingObject: PagingObject<SimplifiedPlaylist>) in
	// Get the playlists via pagingObject.items     
}, failure: { (error) in
	print(error)
})

Search for Tracks

_ = Spartan.search(query: query, type: .track, success: { (pagingObject: PagingObject<SimplifiedTrack>) in
	// Get the tracks via pagingObject.items     
}, failure: { (error) in
	print(error)
})

Get a User’s Profile

_ = Spartan.getUser(id: userId, success: { (user) in
	// Do something with the user
}, failure: { (error) in
	print(error)
})

Get Audio Analysis for a Track

_ = Spartan.getAudioAnaylsis(trackId: trackId, success: { (audiAnalysis) in
	// Do something with the audio analysis
}, failure: { (error) in
	print(error)
})

Get Audio Features for a Track

_ = Spartan.getAudioFeatures(trackId: trackId, success: { (audioFeaturesObject) in
	// Do something with the audio features object  
}, failure: { (error) in
	print(error)
})

Get Audio Features for a Track

_ = Spartan.getAudioFeatures(trackId: trackId, success: { (audioFeaturesObject) in
	// Do something with the audio features object  
}, failure: { (error) in
	print(error)
})

Get Audio Features for Several Tracks

_ = Spartan.getAudioFeatures(trackIds: trackIds, success: { (audioFeaturesObject) in
	// Do something with the audio features objects
}, failure: { (error) in
	print(error)
})

Get a List of Featured Playlists

_ = Spartan.getFeaturedPlaylists(locale: locale, country: .us, timestamp: timestamp, limit: 20, offset: 0, success: { (featuredPlaylistsObject) in
	// Do something with the featured playlists object        
}, failure: { (error) in
	print(error)        
})

Get a List of New Releases

_ = Spartan.getNewReleases(country: .us, limit: 20, offset: 0, success: { (pagingObject) in
	// Get the albums via pagingObject.items
}, failure: { (error) in
	print(error)
})

Get a List of Categories

_ = Spartan.getCategories(success: { (pagingObject) in
	// Get the categories via pagingObject.items
}, failure: { (error) in
	print(error)
})

Get a Category’s Playlists

_ = Spartan.getCategorysPlaylists(categoryId: categoryId, locale: locale, country: .us, limit: 20, offset: 0, success: { (pagingObject) in
	// Get the playlists via pagingObject.items
}, failure: { (error) in
	print(error)
})

Get Current User’s Profile

_ = Spartan.getMe(success: { (user) in
	// Do something with the user object
}, failure: { (error) in
	print(error)
})

Get User’s Followed Artists

_ = Spartan.getMyFollowedArtists(success: { (pagingObject) in
	// Get artists via pagingObject.items
}, failure: { (error) in
	print(error)
})

Follow Artists

_ = Spartan.follow(ids: artistIds, type: .artist, success: {
	// Artists are now followed   
}, failure: { (error) in
	print(error)
})

Follow Users

_ = Spartan.follow(ids: userIds, type: .user, success: {
	// Users are now followed   
}, failure: { (error) in
	print(error)
})

Unfollow Artists

_ = Spartan.unfollow(ids: artistIds, type: .artist, success: {
	// Artists are now unfollowed   
}, failure: { (error) in
	print(error)
})

Unfollow Users

_ = Spartan.unfollow(ids: userIds, type: .user, success: {
	// Users are now unfollowed   
}, failure: { (error) in
	print(error)
})

Check if Current User Follows Artists

_ = Spartan.getIsFollowing(ids: artistIds, type: .artist, success: { (followingBools) in
	// Do something with the followingBools
}, failure: { (error) in
	print(error)        
})

Check if Current User Follows Users

_ = Spartan.getIsFollowing(ids: userIds, type: .user, success: { (followingBools) in
	// Do something with the followingBools
}, failure: { (error) in
	print(error)        
})

Follow a Playlist

_ = Spartan.followPlaylist(ownerId: ownerId, playlistId: playlistId, isPublic: true, success: {
   // Playlist is now followed
}, failure: { (error) in
	print(error)           
})

Unfollow a Playlist

_ = Spartan.unfollowPlaylist(ownerId: ownerId, playlistId: playlistId, success: {
	// Playlist is now unfollowed     
}, failure: { (error) in
	print(error)
})

Get a User’s Saved Tracks

 _ = Spartan.getSavedTracks(limit: 20, offset: 0, market: .us, success: { (pagingObject) in
	// Get the saved tracks via pagingObject.items     
}, failure: { (error) in
	print(error)   
})

Save Tracks for User

_ = Spartan.saveTracks(trackIds: trackIds, success: {
	// Tracks are now saved    
}, failure: { (error) in
	print(error)    
})

Remove User’s Saved Tracks

_ = Spartan.removeSavedTracks(trackIds: trackIds, success: {
	// Tracks are now removed
}, failure: { (error) in
    print(error)
})

Check User’s Saved Tracks

_ = Spartan.tracksAreSaved(trackIds: trackIds, success: { (savedBools) in
    // Do something with the savedBools    
}, failure: { (error) in
    print(error)    
})

Get Current User’s Saved Albums

 _ = Spartan.getSavedAlbums(limit: 20, offset: 0, market: .us, success: { (pagingObject) in
    // Get the saved albums via pagingObject.items    
}, failure: { (error) in
 	print(error)      
})        

Save Albums for User

_ = Spartan.saveAlbums(albumIds: albumIds, success: {
	// Albums are now saved    
}, failure: { (error) in
	print(error)    
})

Remove User’s Saved Albums

_ = Spartan.removeSavedAlbums(albumIds: albumIds, success: {
	// Albums are now removed
}, failure: { (error) in
    print(error)
})

Check User’s Saved Albums

_ = Spartan.albumsAreSaved(albumIds: albumIds, success: { (savedBools) in
    // Do something with the savedBools    
}, failure: { (error) in
    print(error)    
})

Get a User’s Top Artists and Tracks

_ = Spartan.albumsAreSaved(albumIds: albumIds, success: { (savedBools) in
    // Do something with the savedBools    
}, failure: { (error) in
    print(error)    
})

Check User’s Saved Albums

_ = Spartan.albumsAreSaved(albumIds: albumIds, success: { (savedBools) in
    // Do something with the savedBools    
}, failure: { (error) in
    print(error)    
})

Get a User’s Top Artists

_ = Spartan.getMyTopArtists(limit: 20, offset: 0, timeRange: .mediumTerm, success: { (pagingObject) in
	// Get the artists via pagingObject.items
}, failure: { (error) in
	print(error)
})

Get a User’s Top Tracks

_ = Spartan.getMyTopTracks(limit: 20, offset: 0, timeRange: .mediumTerm, success: { (pagingObject) in
	// Get the tracks via pagingObject.items
}, failure: { (error) in
	print(error)
})

Get a List of a User’s Playlists

_ = Spartan.getUsersPlaylists(userId: userId, limit: 20, offset: 0, success: { (pagingObject) in
	// Get the playlists via pagingObject.playlists
}, failure: { (error) in
	print(error)
})

Get a List of Current User’s Playlists

_ = Spartan.getMyPlaylists(limit: 20, offset: 0, success: { (pagingObject) in
	// Get the playlists via pagingObject.items
}, failure: { (error) in
	print(error)
})

Get a Playlist

_ = Spartan.getUsersPlaylist(userId: userId, playlistId: playlistId, fields: fields, market: .us, success: { (playlist) in
	// Do something with the playlist
}, failure: { (error) in
	print(error)
})

Get a Playlist's Tracks

_ = Spartan.getPlaylistTracks(userId: userId, playlistId: playlistId, limit: 20, offset: 0, fields: fields, market: .us, success: { (pagingObject) in
	// Get the playlist tracks via pagingObject.items
}, failure: { (error) in
	print(error)
})

Create a Playlist

_ = Spartan.createPlaylist(userId: userId, name: name, isPublic: true, isCollaborative: true, success: { (playlist) in
	// Do something with the playlist
}, failure: { (error) in
	print(error)
})

Change a Playlist’s Details

_ = Spartan.changePlaylistDetails(userId: userId, playlistId: playlistId, name: name, isPublic: false, isCollaborative: false, success: {
	// The playlist details are now changed
}, failure: { (error) in
	print(error)
})

Add Tracks to a Playlist

_ = Spartan.addTracksToPlaylist(userId: userId, playlistId: playlistId, trackUris: trackUris, success: { (snapshot) in
	// Do something with the snapshot
}, failure: { (error) in
	print(error)
})

Remove Tracks from a Playlist

_ = Spartan.removeTracksFromPlaylist(userId: userId, playlistId: playlistId, trackUris: trackUris, success: { (snapshot) in
	// Do something with the snapshot
}, failure: { (error) in
	print(error)
})

Reorder a Playlist’s Tracks

_ = Spartan.reorderPlaylistsTracks(userId: userId, playlistId: playlistId, rangeStart: rangeStart, rangeLength: rangeLength, insertBefore: insertBefore, snapshotId: snapshotId, success: { (snapshot) in
	// Tracks are now reordered
}, failure: { (error) in
	print(error)
})

Replace a Playlist’s Tracks

_ = Spartan.replacePlaylistsTracks(userId: userId, playlistId: playlistId, trackUris: trackUris, success: {
	// Tracks are now replaced in playlist
}, failure: { (error) in
	print(error)
})

Check if Users Follow a Playlist

_ = Spartan.getUsersAreFollowingPlaylists(ownerId: ownerId, playlistId: playlistId, userIds: userIds, success: { (followings) in
	// Do something with the followings
}) { (error) in
	print(error)
}

Handling Errors

SpartanError objects have a type and error message to help determine what kind of error occurred.

public class SpartanError: NSObject, Mappable {
    private(set) open var type: SpartanErrorType!
    private(set) open var errorMessage:String!
}

If a request suceeds but is invalid, the erorrMessage will be the error message returned directly from Spotify.

For example, if the Spotify error response is:

{
  "error": {
    "status": 401,
    "message": "Invalid access token"
  }
}

The SpartanError object within a failure callback will be:

_ = Spartan.getMe(success: { (user) in

}, failure: { (error) in
	print(error.errorType)     // .unauthorized
	print(error.errorMessage)  // "Invalid access token"
})

So if your access token is expired, you could do something like this:

_ = Spartan.getMe(success: { (user) in

}, failure: { (error) in
	if error.errorType == .unauthorized {
		// Refresh your access token and try again
	}
})

Library Dependencies

Usage Information and Limits

Since Spartan is built on top of the Spotify Web API, click here for more information

Author

Dalton Hinterscher, [email protected]

License

Spartan is available under the MIT license. See the LICENSE file for more info.

Comments
  • Swift 4 AlamofireObjectMapper Dependency

    Swift 4 AlamofireObjectMapper Dependency

    Hello, Just writing to say that the Podfile spec should probably be updated for AlamofireObjectMapper 5 so that users can use swift 4

    [!] Unable to satisfy the following requirements:
    
    - `AlamofireObjectMapper (from `https://github.com/tristanhimmelman/AlamofireObjectMapper.git`, branch `swift-4`)` required by `Podfile`
    - `AlamofireObjectMapper (= 5.0.0)` required by `Podfile.lock`
    - `AlamofireObjectMapper (~> 4.0.1)` required by `Spartan (1.0)`
    
    opened by RudyB 12
  • Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

    Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

    I am executing the following code and I am getting a Fatal error: Unexpectedly found nil while unwrapping an Optional value that references line 57 of SpartanError. Please see the following screenshot for more detailed thread information. This is the only function that does this. It appears that the SpartanError is trying to map a status code and that it is actually nil. Any help on this would be great. https://github.com/Daltron/Spartan/blob/d85450d38be599d11f73f5a7e4547c186a97c946/Spartan/Classes/SpartanError.swift#L57

    Code

    _ = Spartan.getAudioFeatures(trackIds: tracksIDs, success: { (audioFeatures) in
           print(audioFeatures.first?.tempo)
    }, failure: { (error) in
    	Logger.error(error)
    })
    

    Console Output

    πŸ”΅ [AlamoRecordLogger] GET https://api.spotify.com/v1/audio-features?ids=7sO5G9EABYOXQKNPNiE9NR%2C0NKh1STZG1VgnVwntJF3ze%2C4dVpf9jZjcORqGTLUaeYj9%2C2Xqd0wUttjueBfdcltADOv%2C0Gux2yTMWYOlcBUNjGJu5p%2C2vjmyyAnDUzilFYxOlTdO1%2C1OmcAT5Y8eg5bUPv9qJT4R%2C6foxplXS0YEq8cO374Lty4%2C5u6vkDnOyaf8LsteDAj2ub%2C1XRgIKC5TPwo7nWGyKqgG0%2C7zLGHiDWd9T1Rxw4PQCb13%2C1YZfcVLbbJwfizR5cDOp3q%2C7r6LNJT2LqpLpEyZQJPygt%2C57IRaiAB4hBZu3gnNVZC0v%2C6GNifiuBPrKFpwNBYnooFm%2C3ncgNpxLoBQ65ABk4djDyd%2C2vaMWMPMgsWX4fwJiKmdWm%2C0taOCiup4HNG9LmbduVlJj%2C59J5nzL1KniFHnU120dQzt%2C4EsYkJjHKMejYLp54woB9c
    βšͺ️ [AlamoRecordLogger] GET https://api.spotify.com/v1/audio-features?ids=7sO5G9EABYOXQKNPNiE9NR%2C0NKh1STZG1VgnVwntJF3ze%2C4dVpf9jZjcORqGTLUaeYj9%2C2Xqd0wUttjueBfdcltADOv%2C0Gux2yTMWYOlcBUNjGJu5p%2C2vjmyyAnDUzilFYxOlTdO1%2C1OmcAT5Y8eg5bUPv9qJT4R%2C6foxplXS0YEq8cO374Lty4%2C5u6vkDnOyaf8LsteDAj2ub%2C1XRgIKC5TPwo7nWGyKqgG0%2C7zLGHiDWd9T1Rxw4PQCb13%2C1YZfcVLbbJwfizR5cDOp3q%2C7r6LNJT2LqpLpEyZQJPygt%2C57IRaiAB4hBZu3gnNVZC0v%2C6GNifiuBPrKFpwNBYnooFm%2C3ncgNpxLoBQ65ABk4djDyd%2C2vaMWMPMgsWX4fwJiKmdWm%2C0taOCiup4HNG9LmbduVlJj%2C59J5nzL1KniFHnU120dQzt%2C4EsYkJjHKMejYLp54woB9c (200 OK) 16.6 seconds
    (lldb) 
    

    Edited 11/15/17

    Request Output I ran the request in a Paw and I got the following output. As you can see the second element is null. The Spotify API documentation states, "If an object is not found, a null value is returned in the appropriate position. " The track exists in the Spotify library, however audio features for it do not exist. It is possible that Spartan is not properly handling this null value and is searching for an error code that does not exist (because the request was successful) hence the unexpected nil value. Is there a way to get the output of all non null values?

    {
      "audio_features": [
        {
          "danceability": 0.880,
          "energy": 0.428,
          "key": 9,
          "loudness": -8.280,
          "mode": 1,
          "speechiness": 0.206,
          "acousticness": 0.149,
          "instrumentalness": 0.0000507,
          "liveness": 0.114,
          "valence": 0.333,
          "tempo": 100.007,
          "type": "audio_features",
          "id": "7sO5G9EABYOXQKNPNiE9NR",
          "uri": "spotify:track:7sO5G9EABYOXQKNPNiE9NR",
          "track_href": "https://api.spotify.com/v1/tracks/7sO5G9EABYOXQKNPNiE9NR",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/7sO5G9EABYOXQKNPNiE9NR",
          "duration_ms": 172800,
          "time_signature": 4
        },
        null,
        {
          "danceability": 0.797,
          "energy": 0.844,
          "key": 11,
          "loudness": -5.482,
          "mode": 1,
          "speechiness": 0.275,
          "acousticness": 0.0651,
          "instrumentalness": 0,
          "liveness": 0.0870,
          "valence": 0.520,
          "tempo": 170.142,
          "type": "audio_features",
          "id": "4dVpf9jZjcORqGTLUaeYj9",
          "uri": "spotify:track:4dVpf9jZjcORqGTLUaeYj9",
          "track_href": "https://api.spotify.com/v1/tracks/4dVpf9jZjcORqGTLUaeYj9",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/4dVpf9jZjcORqGTLUaeYj9",
          "duration_ms": 173600,
          "time_signature": 4
        },
        {
          "danceability": 0.837,
          "energy": 0.791,
          "key": 1,
          "loudness": -3.824,
          "mode": 1,
          "speechiness": 0.251,
          "acousticness": 0.0199,
          "instrumentalness": 0,
          "liveness": 0.0836,
          "valence": 0.379,
          "tempo": 176.003,
          "type": "audio_features",
          "id": "2Xqd0wUttjueBfdcltADOv",
          "uri": "spotify:track:2Xqd0wUttjueBfdcltADOv",
          "track_href": "https://api.spotify.com/v1/tracks/2Xqd0wUttjueBfdcltADOv",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/2Xqd0wUttjueBfdcltADOv",
          "duration_ms": 211006,
          "time_signature": 4
        },
        {
          "danceability": 0.595,
          "energy": 0.431,
          "key": 10,
          "loudness": -7.282,
          "mode": 1,
          "speechiness": 0.486,
          "acousticness": 0.0101,
          "instrumentalness": 0.0000703,
          "liveness": 0.164,
          "valence": 0.107,
          "tempo": 125.200,
          "type": "audio_features",
          "id": "0Gux2yTMWYOlcBUNjGJu5p",
          "uri": "spotify:track:0Gux2yTMWYOlcBUNjGJu5p",
          "track_href": "https://api.spotify.com/v1/tracks/0Gux2yTMWYOlcBUNjGJu5p",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/0Gux2yTMWYOlcBUNjGJu5p",
          "duration_ms": 227707,
          "time_signature": 3
        },
        {
          "danceability": 0.689,
          "energy": 0.761,
          "key": 1,
          "loudness": -4.939,
          "mode": 1,
          "speechiness": 0.130,
          "acousticness": 0.0485,
          "instrumentalness": 0,
          "liveness": 0.136,
          "valence": 0.698,
          "tempo": 78.514,
          "type": "audio_features",
          "id": "2vjmyyAnDUzilFYxOlTdO1",
          "uri": "spotify:track:2vjmyyAnDUzilFYxOlTdO1",
          "track_href": "https://api.spotify.com/v1/tracks/2vjmyyAnDUzilFYxOlTdO1",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/2vjmyyAnDUzilFYxOlTdO1",
          "duration_ms": 157643,
          "time_signature": 4
        },
        {
          "danceability": 0.580,
          "energy": 0.531,
          "key": 5,
          "loudness": -6.631,
          "mode": 0,
          "speechiness": 0.0776,
          "acousticness": 0.128,
          "instrumentalness": 0.000127,
          "liveness": 0.143,
          "valence": 0.141,
          "tempo": 159.786,
          "type": "audio_features",
          "id": "1OmcAT5Y8eg5bUPv9qJT4R",
          "uri": "spotify:track:1OmcAT5Y8eg5bUPv9qJT4R",
          "track_href": "https://api.spotify.com/v1/tracks/1OmcAT5Y8eg5bUPv9qJT4R",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/1OmcAT5Y8eg5bUPv9qJT4R",
          "duration_ms": 218320,
          "time_signature": 4
        },
        {
          "danceability": 0.950,
          "energy": 0.533,
          "key": 5,
          "loudness": -7.022,
          "mode": 1,
          "speechiness": 0.148,
          "acousticness": 0.247,
          "instrumentalness": 0,
          "liveness": 0.114,
          "valence": 0.675,
          "tempo": 119.869,
          "type": "audio_features",
          "id": "6foxplXS0YEq8cO374Lty4",
          "uri": "spotify:track:6foxplXS0YEq8cO374Lty4",
          "track_href": "https://api.spotify.com/v1/tracks/6foxplXS0YEq8cO374Lty4",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/6foxplXS0YEq8cO374Lty4",
          "duration_ms": 124056,
          "time_signature": 4
        },
        {
          "danceability": 0.782,
          "energy": 0.436,
          "key": 2,
          "loudness": -7.033,
          "mode": 1,
          "speechiness": 0.155,
          "acousticness": 0.331,
          "instrumentalness": 0.0000130,
          "liveness": 0.342,
          "valence": 0.243,
          "tempo": 82.994,
          "type": "audio_features",
          "id": "5u6vkDnOyaf8LsteDAj2ub",
          "uri": "spotify:track:5u6vkDnOyaf8LsteDAj2ub",
          "track_href": "https://api.spotify.com/v1/tracks/5u6vkDnOyaf8LsteDAj2ub",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/5u6vkDnOyaf8LsteDAj2ub",
          "duration_ms": 268933,
          "time_signature": 4
        },
        {
          "danceability": 0.890,
          "energy": 0.633,
          "key": 11,
          "loudness": -5.475,
          "mode": 1,
          "speechiness": 0.168,
          "acousticness": 0.0232,
          "instrumentalness": 0.000343,
          "liveness": 0.0993,
          "valence": 0.425,
          "tempo": 139.948,
          "type": "audio_features",
          "id": "1XRgIKC5TPwo7nWGyKqgG0",
          "uri": "spotify:track:1XRgIKC5TPwo7nWGyKqgG0",
          "track_href": "https://api.spotify.com/v1/tracks/1XRgIKC5TPwo7nWGyKqgG0",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/1XRgIKC5TPwo7nWGyKqgG0",
          "duration_ms": 233087,
          "time_signature": 4
        },
        {
          "danceability": 0.577,
          "energy": 0.593,
          "key": 9,
          "loudness": -4.816,
          "mode": 0,
          "speechiness": 0.137,
          "acousticness": 0.800,
          "instrumentalness": 0,
          "liveness": 0.623,
          "valence": 0.575,
          "tempo": 120.611,
          "type": "audio_features",
          "id": "7zLGHiDWd9T1Rxw4PQCb13",
          "uri": "spotify:track:7zLGHiDWd9T1Rxw4PQCb13",
          "track_href": "https://api.spotify.com/v1/tracks/7zLGHiDWd9T1Rxw4PQCb13",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/7zLGHiDWd9T1Rxw4PQCb13",
          "duration_ms": 303781,
          "time_signature": 5
        },
        {
          "danceability": 0.901,
          "energy": 0.529,
          "key": 2,
          "loudness": -4.443,
          "mode": 1,
          "speechiness": 0.191,
          "acousticness": 0.0519,
          "instrumentalness": 0,
          "liveness": 0.179,
          "valence": 0.146,
          "tempo": 138.003,
          "type": "audio_features",
          "id": "1YZfcVLbbJwfizR5cDOp3q",
          "uri": "spotify:track:1YZfcVLbbJwfizR5cDOp3q",
          "track_href": "https://api.spotify.com/v1/tracks/1YZfcVLbbJwfizR5cDOp3q",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/1YZfcVLbbJwfizR5cDOp3q",
          "duration_ms": 307660,
          "time_signature": 4
        },
        {
          "danceability": 0.860,
          "energy": 0.561,
          "key": 6,
          "loudness": -5.757,
          "mode": 0,
          "speechiness": 0.185,
          "acousticness": 0.274,
          "instrumentalness": 0,
          "liveness": 0.108,
          "valence": 0.354,
          "tempo": 152.984,
          "type": "audio_features",
          "id": "7r6LNJT2LqpLpEyZQJPygt",
          "uri": "spotify:track:7r6LNJT2LqpLpEyZQJPygt",
          "track_href": "https://api.spotify.com/v1/tracks/7r6LNJT2LqpLpEyZQJPygt",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/7r6LNJT2LqpLpEyZQJPygt",
          "duration_ms": 189480,
          "time_signature": 4
        },
        {
          "danceability": 0.658,
          "energy": 0.736,
          "key": 9,
          "loudness": -6.097,
          "mode": 0,
          "speechiness": 0.308,
          "acousticness": 0.113,
          "instrumentalness": 0,
          "liveness": 0.0978,
          "valence": 0.804,
          "tempo": 143.966,
          "type": "audio_features",
          "id": "57IRaiAB4hBZu3gnNVZC0v",
          "uri": "spotify:track:57IRaiAB4hBZu3gnNVZC0v",
          "track_href": "https://api.spotify.com/v1/tracks/57IRaiAB4hBZu3gnNVZC0v",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/57IRaiAB4hBZu3gnNVZC0v",
          "duration_ms": 160000,
          "time_signature": 4
        },
        {
          "danceability": 0.749,
          "energy": 0.671,
          "key": 6,
          "loudness": -5.151,
          "mode": 1,
          "speechiness": 0.0599,
          "acousticness": 0.680,
          "instrumentalness": 0,
          "liveness": 0.167,
          "valence": 0.756,
          "tempo": 81.994,
          "type": "audio_features",
          "id": "6GNifiuBPrKFpwNBYnooFm",
          "uri": "spotify:track:6GNifiuBPrKFpwNBYnooFm",
          "track_href": "https://api.spotify.com/v1/tracks/6GNifiuBPrKFpwNBYnooFm",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/6GNifiuBPrKFpwNBYnooFm",
          "duration_ms": 268864,
          "time_signature": 4
        },
        {
          "danceability": 0.921,
          "energy": 0.467,
          "key": 1,
          "loudness": -8.443,
          "mode": 1,
          "speechiness": 0.119,
          "acousticness": 0.0149,
          "instrumentalness": 0.000238,
          "liveness": 0.334,
          "valence": 0.287,
          "tempo": 135.995,
          "type": "audio_features",
          "id": "3ncgNpxLoBQ65ABk4djDyd",
          "uri": "spotify:track:3ncgNpxLoBQ65ABk4djDyd",
          "track_href": "https://api.spotify.com/v1/tracks/3ncgNpxLoBQ65ABk4djDyd",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/3ncgNpxLoBQ65ABk4djDyd",
          "duration_ms": 191252,
          "time_signature": 4
        },
        {
          "danceability": 0.717,
          "energy": 0.753,
          "key": 7,
          "loudness": -4.609,
          "mode": 0,
          "speechiness": 0.311,
          "acousticness": 0.293,
          "instrumentalness": 0,
          "liveness": 0.158,
          "valence": 0.631,
          "tempo": 169.857,
          "type": "audio_features",
          "id": "2vaMWMPMgsWX4fwJiKmdWm",
          "uri": "spotify:track:2vaMWMPMgsWX4fwJiKmdWm",
          "track_href": "https://api.spotify.com/v1/tracks/2vaMWMPMgsWX4fwJiKmdWm",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/2vaMWMPMgsWX4fwJiKmdWm",
          "duration_ms": 174012,
          "time_signature": 4
        },
        {
          "danceability": 0.721,
          "energy": 0.710,
          "key": 7,
          "loudness": -9.294,
          "mode": 1,
          "speechiness": 0.295,
          "acousticness": 0.000655,
          "instrumentalness": 0,
          "liveness": 0.359,
          "valence": 0.509,
          "tempo": 108.024,
          "type": "audio_features",
          "id": "0taOCiup4HNG9LmbduVlJj",
          "uri": "spotify:track:0taOCiup4HNG9LmbduVlJj",
          "track_href": "https://api.spotify.com/v1/tracks/0taOCiup4HNG9LmbduVlJj",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/0taOCiup4HNG9LmbduVlJj",
          "duration_ms": 156057,
          "time_signature": 5
        },
        {
          "danceability": 0.785,
          "energy": 0.622,
          "key": 8,
          "loudness": -6.741,
          "mode": 1,
          "speechiness": 0.254,
          "acousticness": 0.0133,
          "instrumentalness": 0,
          "liveness": 0.154,
          "valence": 0.466,
          "tempo": 78.475,
          "type": "audio_features",
          "id": "59J5nzL1KniFHnU120dQzt",
          "uri": "spotify:track:59J5nzL1KniFHnU120dQzt",
          "track_href": "https://api.spotify.com/v1/tracks/59J5nzL1KniFHnU120dQzt",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/59J5nzL1KniFHnU120dQzt",
          "duration_ms": 235535,
          "time_signature": 4
        },
        {
          "danceability": 0.930,
          "energy": 0.673,
          "key": 10,
          "loudness": -7.314,
          "mode": 0,
          "speechiness": 0.0733,
          "acousticness": 0.0112,
          "instrumentalness": 0,
          "liveness": 0.138,
          "valence": 0.219,
          "tempo": 134.984,
          "type": "audio_features",
          "id": "4EsYkJjHKMejYLp54woB9c",
          "uri": "spotify:track:4EsYkJjHKMejYLp54woB9c",
          "track_href": "https://api.spotify.com/v1/tracks/4EsYkJjHKMejYLp54woB9c",
          "analysis_url": "https://api.spotify.com/v1/audio-analysis/4EsYkJjHKMejYLp54woB9c",
          "duration_ms": 188563,
          "time_signature": 4
        }
      ]
    }
    
    opened by RudyB 6
  • getNext on pagingObject for PlaylistTracks failing

    getNext on pagingObject for PlaylistTracks failing

    Thank you for creating such an awesome Spotify Client! There's a small issue with getNext being called on a pagingObject dealing with tracks fetched for a playlist. Spartan.getPlaylistTracks() works fine and loads 20 tracks. However after retrieving the pagingObject and calling getNext on it I get fatal error: unexpectedly found nil while unwrapping an Optional value 2017-07-09 19:55:16.220157-0500 Sink[12993:3968613] fatal error: unexpectedly found nil while unwrapping an Optional value. To confirm I called getNext right after fetching the pagingObject as follows ` let _ = Spartan.getPlaylistTracks(userId: userId, playlistId: playlistId, success: { (tracksPage) in

            if tracksPage.canMakeNextRequest {
                tracksPage.getNext(success: { (trackpage) in
                    print("successfully fetched \(trackpage)")
                }, failure: { (error) in
                    print(error.description)
                    print(error.errorMessage)
                })
            }
             //completionHandler(tracksPage)
        }) { (error) in
            print(error.description)
            print(error.errorMessage)
        }
    

    ` and I get the error. I've also attached the stack trace below.

    I am quite certain you'd be able to recreate the error if you fetch tracks for playlist that has number of songs over the the limit of 1 pagingObject and calling getNext on the pagingObject. Any fix or direction would be greatly appreciated!

    screen shot 2017-07-09 at 7 56 24 pm
    opened by kinshukJ 5
  • Add request retrier to SpartanRequestManager

    Add request retrier to SpartanRequestManager

    @RudyB, I'd love for you to take a look at this and tell me what you think. I believe this addresses most of the issues you had in your #11 pull request. A failed request will at most be retried 3 times with this implementation. If the response is nil, this usually indicates that the request failed because there was no active or a very slow internet connection. I noticed in your pull request that you were checking for anything between 500-504. Theres no reason to probably do this as a 500 (Internal Server Error) would happen each time the request is retried due to a server bug and and the same goes for 501 (Not Implemented). If you have any questions, please feel free to ask!

    opened by Daltron 2
  • Retrying requests that fail due to network time outs

    Retrying requests that fail due to network time outs

    I have been noticing request time out issues with Spartan specifically when the user's device network reachability status changes. I recommend that you add support for the Alamofire RequestRetrier protocol that allows the session manager to automatically retry requests if they fail for a specific reason.

    opened by RudyB 2
  • SpartanError: Insufficient client scope

    SpartanError: Insufficient client scope

    I'm trying to create a playlist for the current user and received the error "SpartanError: Insufficient client scope". I have a valid auth token that I refresh and can get the current user's info fine. The scopes I have selected are: static let SCOPE = ["user-read-email", "user-top-read", "playlist-modify-public", "playlist-modify-private"]

    My Code for token request:

    static func accessCodeToAccessToken(code: String, completion: @escaping (Result<Tokens, Error>) -> Void) -> Request {
            
            Request.buildRequest(method: .post,
                                 header: Header.POSTHeader.buildHeader(),
                                 baseURL: SpotifyBaseURL.authBaseURL.rawValue,
                                 path: EndingPath.token.buildPath(),
                                 params: Parameters.codeForToken(accessCode: code).buildParameters()) { (result) in
                
                result.decoding(Tokens.self, completion: completion)
            }
        }
    

    My code calling request in view controller:

    private func getSpotifyAccessCode() {
    
            let urlRequest = client.getSpotifyAccessCodeURL()
            print(urlRequest)
            let scheme = "auth"
            let session = ASWebAuthenticationSession(url: urlRequest, callbackURLScheme: scheme) { (callbackURL, error) in
                guard error == nil, let callbackURL = callbackURL else { return }
    
                let queryItems = URLComponents(string: callbackURL.absoluteString)?.queryItems
                guard let requestAccessCode = queryItems?.first(where: { $0.name == "code" })?.value else { return }
                print(" Code \(requestAccessCode)")
                // exchanges access code to get access token and refresh token
                self.client.call(request: .accessCodeToAccessToken(code: requestAccessCode, completion: { (token) in
                    switch token {
    
                    case .failure(let error):
                        print("ERROR: ", error)
                    case .success(let token):
                        UserDefaults.standard.set(token.accessToken, forKey: "token")
                        UserDefaults.standard.set(token.refreshToken, forKey: "refresh_token")
                        print("SUCCESS")
                        
                        DispatchQueue.main.async {
                            self.goToApp()
                        }
                    }
                }))
    
            }
    
            session.presentationContextProvider = self
            session.start()
    
        }
    

    Create playlist code:

     func createPlaylist(user: UserModel) {
            
            let name = "Test"
            
            let createPlaylist = Spartan.createPlaylist(userId: user.id, name: name, isPublic: true) { (playlist) in
                print("PLAYLIST CREATED")
                
            } failure: { (error) in
                print("Error creating playlist: ", error)
            }
    
        }
    

    Sorry if this isn't enough information please let me know.

    opened by timmyvc123 1
  • Search: pagingObject.canMakeNextRequest condition does not work

    Search: pagingObject.canMakeNextRequest condition does not work

    Hi, Yesterday the pagingObject.canMakeNextRequest condition stopped working as expected.

    It does end at offset=850 but according to the Spotify Web API Console there should be a total of 95902 results.

    If I remove the canMakeNextRequest condition and call searchNewAlbums regardless of this value, it runs to the 10000 offset limit just fine.

    Here is the code snippet:

     _ = Spartan.search(query: "tag:new", type: .album, market: .de, limit: 50, offset: offset, success: { (pagingObject: PagingObject<SimplifiedAlbum>) in
                // temporary workaround for null objects in search results
                if (pagingObject.items != nil){
                    for album in pagingObject.items {
                            self.releasesIdSet.insert(album.id as! String)
                    }
                    
                    if pagingObject.canMakeNextRequest {
                        self.searchNewAlbums(offset: (offset+pagingObject.limit))
                    }
                } else {
                    print("PAGING OBJECT == NIL")
                    // TODO do error handling
                }
            }, failure: { (error) in
                print(error)
                // TODO do error handling
            })
    

    The last get request until it finishes and there is no error message: πŸ”΅ [AlamoRecordLogger] GET https://api.spotify.com/v1/search?limit=50&market=DE&offset=850&q=tag%3Anew&type=album

    And a screenshot from the Web API Console: bildschirmfoto 2018-10-20 um 09 40 19

    opened by tomaculum 1
  • Get track id from Paging Object.

    Get track id from Paging Object.

    I am wondering if there is a way to get the track id from a Paging Object. Here is the method I am using:

    playSpotifyURI("spotify:track:" + track, startingWith: 0, startingWithPosition: 0)
    

    This works if track is 58s6EuEYJdlb0kO7awm3Vp

    I use this to get an array of tracks:

    Spartan.search(query: search, type: .playlist, success:...
    

    This returns a Paging Object which I get the items array from. The items array contains an href which looks like this https://api.spotify.com/v1/users/spotify/playlists/37i9dQZF1DX4y8h9WqDPAE/tracks. Unfortunately this does not contain the exact track id, it only gives the playlist id.

    opened by zoecarver 1
  • Suddenly Spartan started crashing my app

    Suddenly Spartan started crashing my app

    I'm not sure if something changed in Spotify API, but performing some requests causes bad crashes. For example, requesting a track with func getTrack(id: String returns invalid object with missing values, for example durationMs is nil, but it never should be. A lot of users are reporting that app started crashing.

    Zrzut ekranu 2020-04-12 o 10 38 30
    opened by wiencheck 0
  • Add conformance to AFire protocol `RequestRetrier`

    Add conformance to AFire protocol `RequestRetrier`

    I implemented a new class SpartanRequestRetrier that conforms to the Alamofire protocol SpartanRequestRetrier. I then configured it in SpartanRequestManager class. If the request fails due to a server error, the request will be retried in 1 second.

    My concerns:

    • I am afraid this can end up becoming endlessly recursive.
    • We should give the user the option to enable this feature.
    • Are there more error codes that should be retried?
    • Is 1 second the proper amount of time to wait to retry?
    opened by RudyB 0
  • Playlist Image Issue

    Playlist Image Issue

    Hi Daltron,

    I found an issue with playlist images. After getting root user's playlists using Spartan.getMyPlaylists(), for playlists that don't have an image, I get a random image from one of my other playlists. Moreover, all playlists after that one return random images from other playlists even though they have their own image on Spotify. Any help on this would be much appreciated!

    Thanks :)

    opened by kinshukJ 0
  • Using `getArtists` method causes crash

    Using `getArtists` method causes crash

    I was using the getArtists(ids: [String]...) method with chunks of artist identifiers, each counting 50, as API states that's the limit and in some cases the app crashes. See attached screenshot.

    Zrzut ekranu 2020-04-12 o 10 38 30

    The crash is only happening with some artists, as users reported. Not all of them experienced the crash.

    opened by wiencheck 0
  • `getTrack` returns invalid Track object whereas `getTracks` works as expected

    `getTrack` returns invalid Track object whereas `getTracks` works as expected

    When using Spartan method getTrack(id: String...) Track object is received but most of its properties are nil, when I checked the uri of the object it turns out that actually an Album object was received but when Spartan tried to map it to Track most of the properties were nil.

    See the Xcode Quicklook of the object Zrzut ekranu 2020-04-13 o 14 06 43

    You can see that the object is in fact an album. I think I'm calling the method correctly

    Zrzut ekranu 2020-04-13 o 14 07 24

    Although this method gives wrong results, using getTracks(ids: [String]...) works correctly and I'll be using it to get single tracks from the API

    opened by wiencheck 0
  • How to use Spartan

    How to use Spartan

    I am creating an app that exports playlists to Spotify. Currently, I have an array of Spotify song URIs. I have no knowledge of how the Spotify SDK works and I was wondering how I would use Spartan to a) set up a connection with the user and 2) create a playlist from the app.

    opened by theg3ntlem4n 0
  • getTrack() returns a Track only filled with SimplifiedTrack data

    getTrack() returns a Track only filled with SimplifiedTrack data

    Spartan.getTrack(id: "11dFghVXANMlKmJXsNCbNl", success: { (track) in self.currentSong = track // FIXME: gets the album instead of the song self.miniPlayer?.configure(song: self.currentSong) }) { (error) in print("Error while fetching the track: \(error.description)") } The above code is the one I'm currently using to retrieve a Track, but no album data is filled in it, is this behaviour intentional?

    captura de pantalla 2018-11-30 a las 17 44 51

    This is the Track returned by Spartan

    Using the latest version available in Cocoapods, Swift 4.0 and Xcode 10.1

    opened by olmedocr 3
  • update read me to become more user-friendly

    update read me to become more user-friendly

    add agenda, add list and drop down blocks to usage examples section

    I add it because it is difficult and a bit boring to scroll all the usage examples list when u want to find something in particular. Hope it will be helpful :)

    opened by hellensoloviy 1
Releases(1.2.1)
Owner
Dalton Hinterscher
Blessed husband and father who loves everything about iOS.
Dalton Hinterscher
Easy and powerful way to interact with VK API for iOS and macOS

Easy and powerful way to interact with VK API for iOS and macOS. Key features ?? It's not ios-vk-sdk ?? ?? One library for iOS and mac OS ?? ?? Fully

null 260 Dec 22, 2022
Instagram API client written in Swift

SwiftInstagram is a wrapper for the Instagram API written in Swift. It allows you to authenticate users and request data from Instagram effortlessly.

Ander Goig 579 Dec 31, 2022
Questrade API written in Swift.

QuestradeAPI Getting Started The QuestAPI is made up of two main concepts: ResponseProviders API ResponseProviders The job of the provider is to retur

Eli Slade 2 Mar 15, 2022
A stable, mature and comprehensive Objective-C library for Twitter REST API 1.1

STTwitter A stable, mature and comprehensive Objective-C library for Twitter REST API 1.1 Like a FOSS version of Twitter Fabric TwitterKit, without th

Nicolas Seriot 1k Nov 30, 2022
A Swift library for the Forecast.io Dark Sky API

Requirements To use ForecastIO, all you need is an API key for the Dark Sky API. ForecastIO supports iOS (β‰₯9.0), macOS (β‰₯10.10), watchOS (β‰₯2.0), and t

Satyam Ghodasara 163 Jul 26, 2022
Unified API Library for: Cloud Storage, Social Log-In, Social Interaction, Payment, Email, SMS, POIs, Video & Messaging.

Unified API Library for: Cloud Storage, Social Log-In, Social Interaction, Payment, Email, SMS, POIs, Video & Messaging. Included services are Dropbox, Google Drive, OneDrive, OneDrive for Business, Box, Egnyte, PayPal, Stripe, Google Places, Foursquare, Yelp, YouTube, Vimeo, Twitch, Facebook Messenger, Telegram, Line, Viber, Facebook, GitHub, Google+, LinkedIn, Slack, Twitter, Windows Live, Yahoo, Mailjet, Sendgrid, Twilio, Nexmo, Twizo.

CloudRail 195 Nov 29, 2021
A Swift wrapper for Foursquare API. iOS and OSX.

Das Quadrat Das Quadrat is Foursquare API wrapper written in Swift. Features Supports iOS and OSX. Covers all API endpoints. Authorization process imp

Constantine Fry 171 Jun 18, 2022
A Dropbox v2 client library written in Objective-C

TJDropbox TJDropbox is a Dropbox v2 client library written in Objective-C. When Dropbox originally announced their v2 API they included only a Swift c

Tim Johnsen 61 Dec 21, 2022
The Waterwheel Swift SDK provides classes to natively connect iOS, macOS, tvOS, and watchOS applications to Drupal 7 and 8.

Waterwheel Swift SDK for Drupal Waterwheel makes using Drupal as a backend with iOS, macOS, tvOS, or watchOS enjoyable by combining the most used feat

Kyle Browning 414 Jul 26, 2022
Unofficial Dribbble iOS wrapper allows you to integrate Dribble API into iOS application (Designer, Shot, Comment, User Story, Like, Follow)

DribbbleSDK DribbbleSDK is easy-to-use iOS wrapper for Dribbble SDK. We're working hard to complete the full coverage of available methods and make th

Agilie Team 74 Dec 2, 2020
Wanikani-swift - Unofficial Swift client for the WaniKani API

WaniKani A Swift library and client for the WaniKani REST API. It currently supp

Aaron Sky 5 Oct 28, 2022
Studio Ghibli Movie database in Swift. For iOS, iPadOS, and MacOS.

Ghibliii Studio Ghibli Movie database in Swift *Colours are shifted due to compression About This started as a way for me to do something in this quar

Kevin Laminto 19 Dec 9, 2022
Giphy API client for iOS in Objective-C

Giphy-iOS Giphy-iOS is a Giphy API client for iOS in Objective-C. Usage To run the example project, clone the repo, and run pod install from the Examp

alex choi 52 Jul 11, 2019
Unofficial GitHub API client in Swift

Github.swift ❀️ Support my apps ❀️ Push Hero - pure Swift native macOS application to test push notifications PastePal - Pasteboard, note and shortcut

Khoa 184 Nov 25, 2022
A swift SDK for Medium's OAuth2 API

Medium SDK - Swift A library to allow access to Medium API for any Swift iOS application. Features Medium.com authorization & token handling Login sta

null 11 Jan 22, 2022
Swift Wrapper For Bittrex API

BittrexApiKit Swift client for Bittrex api. It support all APIs with most recent changes. more info here let api = Bittrex(apikey: "api key", secretke

Saeid 8 Apr 5, 2021
A Slack API Client for the Perfect Server-Side Swift Framework

PerfectSlackAPIClient is an API Client to access the Slack API from your Perfect Server Side Swift application. It is build on top of PerfectAPIClient

Cap.ι›ͺγƒŽδΈ‹ε…«εΉ‘ 2 Dec 1, 2019
Endless-Api-OP - A swift server for Endless

Endless-Api Introduce Hear is endless-api open source library. Endless-Api is us

Underthestars-zhy 1 Jan 30, 2022
The best way to use the Zora API in your Swift projects.

ZoraKit The best way to use the Zora API in your Swift projects. Disclaimer This is still very much a work in progress, and is really a proof-of-conce

MBLA 6 Sep 23, 2022