Apple News Format API in Swift: GET Channel, Section and Article Data (Swift 4)


You can use the online tool to create your own Apple News posts through iCloud.com, no problem. You can also look to Apple's python scripts as a way to upload, download and delete articles. If it's a Swift equivalent you want, then you'll need to roll your own. And that's what I start to do here having covered a couple of side notes first.

Side notes about Python

To use the Python scripts I needed to use easy_install to install requests on macOS.

Side notes on News Preview

To use the News Preview app, and start exploring the Apple News Format, you need to have Java 8 installed, Xcode and Xcode command line tools, and a channel id (although Apple does provide a preview channel id in the readme file if you're not yet signed up to Apple News as a content provider).

Once you have everything installed grab the example articles and start learning.

Python: GET Channel Data

This is the Python code that Apple provides in its documentation for returning channel data:
#!/usr/bin/python

import requests
import base64
from hashlib import sha256
import hmac
from datetime import datetime
channel_id = '[YOUR CHANNEL ID]'
api_key_id = '[YOUR CHANNEL KEY]'
api_key_secret = '[YOUR SECRET KEY]'
url = 'https://news-api.apple.com/channels/%s' % channel_id
date = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
canonical_request = 'GET' + url + str(date)
key = base64.b64decode(api_key_secret)
hashed = hmac.new(key, canonical_request, sha256)
signature = hashed.digest().encode("base64").rstrip('\n')
authorization = 'HHMAC; key=%s; signature=%s; date=%s' % (api_key_id, str(signature), date)
headers = {'Authorization': authorization}
response = requests.get(url, headers=headers)
print response.text

Swift 4: GET Channel Data

And here is a translation into Swift 4.

One of the first things we are going to need to generate is the UTC time string, so let's write a method for that:
func utcTime() -> String {
    if #available(OSX 10.12, *) {
        let formatter = ISO8601DateFormatter()
        // GMT is default time zone
        return formatter.string(from: Date())
    } else {
        let dateFormatter = DateFormatter()
        //The Z at the end of your string represents Zulu which is UTC;
        let timeZone = TimeZone(secondsFromGMT: 0)
        dateFormatter.timeZone = timeZone
        dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'"
        let time = dateFormatter.string(from: Date())
        return time
    }
}
For macOS 10.12 (and iOS 10.0) forwards ISO8601DateFormatter makes this a breeze, while the old way is slightly trickier because we need to supply the string formatting ourselves. But an even trickier task is the ability to create a SHA256 digest using a key and returning a Base64 encoded string. In order to do this the first thing you'll need is to create a bridging header and import CommonCrypto, the methods for this can then be written as String extensions.
extension String {
    // see http://stackoverflow.com/a/24411522/1694526
    func base64ToByteArray() -> [UInt8]? {
        if let data = Data(base64Encoded: self, options: []) {
            return [UInt8](data)
        }
        return nil // Invalid input
    }
    
    func createAppleSignature(key:String) -> String? {
        if let keyData = key.base64ToByteArray(),
            let paramData = self.data(using: String.Encoding.utf8),
            let hash = NSMutableData(length: Int(CC_SHA256_DIGEST_LENGTH)) {
            CCHmac(UInt32(kCCHmacAlgSHA256), keyData, keyData.count, [UInt8](paramData), paramData.count, hash.mutableBytes)
            return hash.base64EncodedString(options: [])
        }
        return nil
    }
}
Now the additional bits and pieces are out the way we're ready to make the request to Apple News. And here it all is packaged up in a method:

func channelData(channelID:String, apiKeyID: String, apiKeySecret:String) {
    let url = "https://news-api.apple.com/channels/\(channelID)"
    let date = utcTime()
    let canonical_request = "GET\(url)\(date)"
        
    // don't worry about the key and hashed lines of Python code here, we cover all that in the createAppleSignature code
    if let signature = canonical_request.createAppleSignature(key: apiKeySecret),
        let nsURL = URL(string: url) {
        let authorization = "HHMAC; key=\(apiKeyID); signature=\(signature); date=\(date)"
        let headers = ["Authorization": authorization]
            
        // now make NSURLSession GET request
        let session = URLSession(configuration: URLSessionConfiguration.ephemeral)
        var request = URLRequest(url: nsURL)
        request.allHTTPHeaderFields = headers
        request.httpMethod = "GET"
        let dataTask = session.dataTask(with: request, completionHandler: {(data, response, error) in
            if (error != nil) {
                print("error")
            }
            if let response = response as? HTTPURLResponse {
                print(response.statusCode)
                print(response.allHeaderFields)
            }
            if let data = data {
                print(String(data: data, encoding: String.Encoding.utf8) ?? "Fail")
            }
        })
        dataTask.resume()
    }
        
}
Now assuming you have everything set up with Apple to make use of Apple News Format and their API, and have an ID, Key and Secret, you should now be able to write the code to make all this work at the touch of a button. Something like:
@IBAction func channelDataReceive(_ sender: Any) {
    let channelID = ""
    let keyID = ""
    let secret = ""

    channelData(channelID: channelID, apiKeyID: keyID, apiKeySecret: secret)
}
which once you've inserted your ID, Key and Secret will work once you've hooked the method up to a button provided you take one addition step, which is to open up the app's permissions for communication with a server. If you are writing a macOS app, this can be done in the app's Capabilities by checking incoming and outgoing connections. (If you're writing for iOS, see Appendix to this post on App Transport Security.)

The output you'll receive back is a JSON dictionary. This is a good start because making requests and receiving/sending data are the basis of what needs to be done with Apple News.

Extending to retrieve Section and Article Data

Not only can you retrieve the core channel data in this way but you can also retrieve section and article data with a slight modification by first creating a ChannelInfo enum:
enum ChannelInfo {
    case Data, Section, Article
}
and then requiring our channelData: to take a type:
func channelData(type:ChannelInfo, channelID:String, apiKeyID: String, apiKeySecret:String) {
    let url = type == .Data ? "https://news-api.apple.com/channels/\(channelID)" : type == .Article ? "https://news-api.apple.com/channels/\(channelID)/articles" : "https://news-api.apple.com/channels/\(channelID)/sections"
    let date = utcTime()
    let canonical_request = "GET\(url)\(date)"
        
    // don't worry about the key and hashed lines of Python code here, we cover all that in the createAppleSignature code
    if let signature = canonical_request.createAppleSignature(key: apiKeySecret),
        let nsURL = URL(string: url) {
        let authorization = "HHMAC; key=\(apiKeyID); signature=\(signature); date=\(date)"
        let headers = ["Authorization": authorization]
            
        // now make NSURLSession GET request
        let session = URLSession(configuration: URLSessionConfiguration.ephemeral)
        var request = URLRequest(url: nsURL)
        request.allHTTPHeaderFields = headers
        request.httpMethod = "GET"
        let dataTask = session.dataTask(with: request, completionHandler: {(data, response, error) in
            if (error != nil) {
                print("error")
            }
            if let response = response as? HTTPURLResponse {
                print(response.statusCode)
                print(response.allHeaderFields)
            }
            if let data = data {
                print(String(data: data, encoding: String.Encoding.utf8) ?? "Fail")
            }
        })
        dataTask.resume()
    }
        
}
All that then changes is the URL, which we can change depending on the info type requested. This means a slight alteration to our action:
@IBAction func channelDataReceive(_ sender: Any) {
    let channelID = ""
    let keyID = ""
    let secret = ""

    channelData(type:.Data, channelID: channelID, apiKeyID: keyID, apiKeySecret: secret)
}
And a repetition of this method for each button changing the type – or a tagging (or reading of the) buttons, approach it how you will – just know that for retrieving sections you use the .Section case and for articles the .Article case and all will be well.

I hope to return to this topic and soon focus on uploading articles using Swift. Meanwhile here are some handy links:

Tools

News Preview app (Apple)

Guides

News Publishing Guide (Apple)
Formatting Guide (Apple)
API Reference (Apple)

Examples

Example Articles (Apple, see bottom of page)

Resources

Apple News Resources and Contact (Apple)

Comments