• Custom Error with localizedDescription
    enum EncyptError: LocalizedError {
        case error(String)
        
        public var errorDescription: String? {
            switch self {
            
            case .error(let message):
                return message
            }
        }
    }
    
  • Converting an audio (PCM) CMSampleBuffer to a Data instance

    Assumes you set up your AVCaptureAudioDataOutput something like this (the key assumption is the bit depth):

    let audioOutput = AVCaptureAudioDataOutput()
    audioOutput.audioSettings =  [
        AVFormatIDKey: kAudioFormatLinearPCM,
        AVNumberOfChannelsKey: 1,
        AVSampleRateKey: 16000.0,
    
        AVLinearPCMBitDepthKey: 16,
        AVLinearPCMIsBigEndianKey: false,
        AVLinearPCMIsFloatKey: false,
        AVLinearPCMIsNonInterleaved: false
    ]
    

    This is the setSampleBufferDelegate delegate:

    func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
        let byteCount = sampleBuffer.numSamples * sampleBuffer.sampleSize(at: 0)
    
        do {
            let unsafeRawPointer = UnsafeMutableRawPointer.allocate(byteCount: byteCount, alignment: 0)
            let pcmBufferPointer = UnsafeMutablePointer<AudioBufferList>.allocate(capacity: 1)
            pcmBufferPointer.initialize(to: AudioBufferList(mNumberBuffers: 1, mBuffers: AudioBuffer(mNumberChannels: 1, mDataByteSize: UInt32(byteCount), mData: unsafeRawPointer)))
    
            try sampleBuffer.copyPCMData(fromRange: 0..<sampleBuffer.numSamples, into: pcmBufferPointer)
    
            let data = Data(bytes: unsafeRawPointer, count: byteCount)
            // Do something with the data
    
        } catch {
            logE("Error converting buffer: \(error.localizedDescription)")
        }
    
    

    You will likely not want to allocate the buffer on each callback.

  • Overriding default Help menu in SwiftUI Mac App
    import SwiftUI
    
    @main
    struct AppleWatchAlexaApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
                    .resizedonMacOS(widthPercent: 1)
            }
            .commands {
                CommandGroup(replacing: .help) {
                    NavigationLink("Voice in a Can Help", destination:
                                    WebView(url: URL(string: "https://voiceinacan.com/ios-faq/")!)
                    )
                }
            }
        }
    }
    
    
  • Date as yyyymmddhhss
    extension Date {
        var yyymmddhhss: String {
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "yyyMMddhhmmss"
            return dateFormatter.string(from: self)
        }
    }
    
  • 'Unable to present. Please file a bug.' with multiple NavigationLinks

    This is a known issue but the workaround of adding an empty NavigationLink didn’t work for me.

    On my Mac app when I have multiple NavigationLinks I get Unable to present. Please file a bug. when I tap on more than one.

    My hacky solution is to create a NavigationButton that replaces the NavigationLink

    struct NavigationButton<Destination>: View where Destination: View {
        let text: String
        let destination: Destination
        @State var showing = false
        
        init(_ text: String, destination: Destination) {
            self.text = text
            self.destination = destination
        }
        
        var body: some View {
            ZStack {
                if showing {
                    NavigationLink(text, destination: destination, isActive: $showing)
                        .opacity(0)
                }
                Button(text) {
                    showing = true
                }
            }
        }
    }
    
  • Decoding a JWT in Swift

    Surprised it isn’t built-in. I’ve used JWTDecode.swift to decode tokens:

    guard let jwt = try? decode(jwt: token),
          let expires = jwt.expiresAt else {
        print("Can't decode jwt")
        return
    }
    
  • if or guard on enum with a parameter instead of using a switch

    Using an if or a guard instead of a switch

    guard case let .onLogin(account) = gigyaEvent,
          let uid = account.UID else {
        return
    }
    
  • Enabling Core Data Encryption in Swift

    From https://stackoverflow.com/a/65525585/3390

    container = NSPersistentContainer(name: "Model")
    container.persistentStoreDescriptions.first!.setOption(FileProtectionType.complete as NSObject,
                                                           forKey: NSPersistentStoreFileProtectionKey)
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in
    
  • Sharing files

    Use share func from https://stackoverflow.com/a/59515229/3390

  • Creating ZIP files in Swift

    I’ve used ZIPFoundation

    let archive = Archive(accessMode: .create) // Create it in memory
    
    // Add a file
    try archive?.addEntry(with: "export/photos/\(photo_id)", fileURL: filename)
    
    
    // Add string as a file
    try archive?.addEntry(with: "export/data.csv", type: .file, uncompressedSize: UInt32(output.count)) { position, size in
                    return output.data(using: .utf8)!
                }
    
    // Write ZIP as a file
    if let data = archive?.data {
        let exportFile = FileManager.default.temporaryDirectory.appendingPathComponent("export.zip")
        print("Path: \(exportFile.path)")
        try data.write(to: exportFile)
    }
    
  • Add hours from one date to another date
    let date = Calendar.current.date(byAdding: DateComponents(hour: Calendar.current.component(.hour, from: otherDate)), to: thisDate)
    
  • Delete all local git branches except main
    git for-each-ref --format '%(refname:short)' refs/heads | grep -v main | xargs git branch -D
    
  • Recording a simulator video
    xcrun simctl io booted recordVideo --force --codec h264 fred.mov
    
  • Stop iPad Split View in SwiftUI

    Set navigationViewStyle to StackNavigationViewStyle

    NavigationView {
        TheTabView()
    }
    .navigationViewStyle(StackNavigationViewStyle())
    
  • Launching processes in Swift

    Launching multiple commands in Swift

            do {
                try writePost()
                let add = Process()
                add.currentDirectoryPath = root
                add.executableURL = URL(fileURLWithPath: git)
                add.arguments = ["add", "."]
                try add.run()
                
                let commit = Process()
                commit.currentDirectoryPath = root
                commit.executableURL = URL(fileURLWithPath: git)
                commit.arguments = ["commit","-m", "\"title\""]
                try commit.run()
                commit.waitUntilExit()
                
                let push = Process()
                push.currentDirectoryPath = root
                push.executableURL = URL(fileURLWithPath: git)
                push.arguments = ["push"]
                try push.run()
                push.waitUntilExit()
                
            } catch {
                errorMessage =  "Error: \(error)"
            }
    
  • XCTest can't tap SwiftUI Toggle

    Using tap on a SwiftUI Toggle wasn’t working for me:

        let app = XCUIApplication()
        let sw = app.switches["My SwiftUI Toggle"]
        XCTAssert(sw.value as! String == "0")
        sw.tap()
        XCTAssert(sw.value as! String == "1")
    

    The above fails. I think this is because tapping the label part of the Toggle does nothing. Instead, I try to tap the switch part of it, which works:

        let app = XCUIApplication()
        let sw = app.switches["My SwiftUI Toggle"]
        XCTAssert(sw.value as! String == "0")
        sw.coordinate(withNormalizedOffset: CGVector.zero).withOffset(CGVector(dx: sw.frame.width - 10, dy: sw.frame.height / 2)).tap()
        XCTAssert(sw.value as! String == "1")
    

    Now it works - I tap in the vertical center, on the right side of the switch, minus ten points.

  • Formatting a single swift date field in JSON

    You want to use one encoding strategy overall to encode JSON dates, but one particular field needs to be formatted using a different formatting?

    Normally you’d set the encoding strategy for the whole encoding:

        let encoder = JSONEncoder()
        encoder.dateEncodingStrategy = .millisecondsSince1970 // or iso8601
        encoder.encode(myObject)
    

    But what if deep down in the structure or class, one date field needs to be formatted a different way, such as the timeStamp field here?

        struct GeolocationState: EventBase {
            let timestamp: Date
            let coordinate: Coordinate
            let altitude: Altitude?
            let heading: Heading?
            let speed: Speed?
        }
    

    My solution is to wrap the date in my own struct, and then I can control the encoding for that:

        struct GeolocationState: EventBase {
            let timestamp: ISO8601Date
            let coordinate: Coordinate
            let altitude: Altitude?
            let heading: Heading?
            let speed: Speed?
        }
    
        struct ISO8601Date: Encodable {
            static let formatter = ISO8601DateFormatter()
            let date: Date
            init(_ date: Date) { self.date = date }        
    
            enum CodingKeys: CodingKey { case date }
            
            func encode(to encoder: Encoder ) throws {
                var container = encoder.singleValueContainer()
                try container.encode(ISO8601FormattedDate.formatter.string(from: date))
            }
        }
    
  • Detecting when state variable changes

    I tried using willSet or didSet on an @State variable, but it didn’t work

    @State var consumed: Bool = false {
        didSet {
            print("never called")
        }
    }
    

    Instead what works is to use the onChange event:

    Toggle("My toggle", isOn: $consumed)
        .onChange(of: self.consumed) { v in
            print("is called")
        }
    
    
  • isActive not working to go back when three levels deep

    In my app I am using NavigationLink.isActive to programatically go back by setting a bound variable to false. I was doing this several levels deep, and although I could initially go back, once I pushed several views onto the stack, and then went back, I found the back button stopped working on one of the pages.

    I also noticed this error:

    2021-01-13 18:02:26.660739+0100 Resolute[6210:333764] [Assert] displayModeButtonItem is internally managed and not exposed for DoubleColumn style. Returning an empty, disconnected UIBarButtonItem to fulfill the non-null contract.
    

    This Stack Overflow page suggested setting the navigationViewStyle to StackNavigationViewStyle:

    NavigationView {
        ...
    }
    .navigationViewStyle(StackNavigationViewStyle())
    

    This both got rid of the error, and the back button started working (setting the isActive bound variable to false)

  • VS Code Regex to duplicate the text on each line

    Replace (.*) with $1 $1

  • Adding days/weeks/months to dates
    let today = Date()
    let date = Calendar.current.date(byAdding: .day, value: 7, to: today)!
    
  • Converting an object to a raw pointer and back

    To a pointer

    let rawPointer = unsafeBitCast(anObject, to: UnsafeMutableRawPointer.self)
    

    … and back

    let anObject = Unmanaged<TheClassObjectsClass>.fromOpaque(UnsafeRawPointer(rawPointer)).takeUnretainedValue()
    
  • A failed attempt to get an AVAsset from a data uri

    This is my attempt. It doesn’t work. The best workaround is to write the data to file first

    var base64 = data.base64EncodedString(options: [])
    
    base64 = base64.replacingOccurrences(of: "=", with: "")
    base64 = base64.replacingOccurrences(of: "+", with: "-")
    base64 = base64.replacingOccurrences(of: "/", with: "_")
    
    let dataURL = URL(string: "data:audio/mpeg;base64,\(base64)")!
    let asset = AVAsset(url: dataURL)
    let millis =  Int(CMTimeGetSeconds(asset.duration)*1000) // Returns zero
    
  • When JSON encoding a Double there are too many decimal places

    If you are trying to generate JSON and a double is encoded with loads of decimal places and weird “1” at the end, like this:

    struct Coordinate: Encodable {
        let latitudeInDegrees: Double
        let longitudeInDegrees: Double
        let accuracyInMeters: Double
    }
    let coordinate = Coordinate(latitudeInDegrees: 37.785834, longitudeInDegrees: -122.406417, accuracyInMeters: 5)
    

    Generates JSON:

    {
        "coordinate" : {
            "accuracyInMeters" : 5,
            "latitudeInDegrees" : 37.785834000000001,
            "longitudeInDegrees" : -122.406417
        }
    }
    

    Change the type from Double to Decimal:

    struct Coordinate: Encodable {
        let latitudeInDegrees: Decimal
        let longitudeInDegrees: Decimal
        let accuracyInMeters: Decimal
    }
    
  • Making JSON have both sorted keys and pretty printed
    let encoder = JSONEncoder()
    encoder.outputFormatting = JSONEncoder.OutputFormatting([.prettyPrinted, .sortedKeys])
    
  • Adding a framework to a test target

    I wanted to add the AnyCodable to both my iOS project, and the test project that tests it.

    When I added it to just my iOS project I got the error Undefined symbol: nominal type descriptor for AnyCodable.AnyCodable when trying to run tests.

    My solution was to remove the framework, and then re-add it, this time specifying the test target as the target into which the framework should be added.

    Once I did that, I was then able to go to my iOS project’s target, and in the General pane, scroll down to Frameworks, Libraries, and Embedded Content and add it there.

  • Swift does not preserve field order when encoding json

    Converting code from .NET to Swift I’ve discovered that Swift does not preserve the order in which fields are declared when encoding JSON.

    There is a bug about it: https://bugs.swift.org/browse/SR-7992

    Support forums are full of people saying the order should not matter, but the reality is that some servers, over which we have no control, require a specific ordering, and it would be nice if Swift supported specifying or even preserving the field order when encoding JSON.

    My solution, in a very limited situation, was to use string interpolation to insert my values into a pre-encoded JSON string which had the correct ordering.

  • Indexing into a string
    // [3] = (key = "Content-Type", value = "multipart/related; boundary=------abcde123; type=application/json")
    let boundaryKey = "boundary="
    guard let contentType = headers["Content-Type"] else {
        logE("opened", "No content type header)")
        return 
    }
    
    let contentTypeParts = contentType.split(separator: ";").map {$0.trimmingCharacters(in: .whitespaces) }
    guard let boundaryPart = contentTypeParts.first(where: {  $0.starts(with: boundaryKey)}) else {
        logE("opened", "Can't find boundary in \(contentType)")
        return
    }
    
    let boundary = String(boundaryPart[boundaryPart.index(boundaryPart.startIndex, offsetBy: boundaryKey.count)...])
    
    
  • Adding a font to a SwiftUI app

    Download and unzip the font, for example from https://fonts.google.com/specimen/Poppins

    Drag the .ttf (for example Poppins-Bold.ttf) file into your Xcode project, perhaps in a folder called Fonts and ensure that it is included in your project’s target (select it and verify Target Membership in the file properties).

    Edit your info.plist and add a top level Fonts provided by application node, and then a child Item 0 node with a value of the name of your font, for example Poppins-Bold.ttf.

    Use the font in your app:

    Text("hello")
        .font(.custom("Poppins-Bold", size: 48))
    
  • SwiftUI Radial Gradient with hex
    background: linear-gradient(197.54deg, #D0A0C3 0%, #D94E42 52.73%, #FAEF64 100%);
    
    struct Hello: View {
        var body: some View {
            VStack() {
                Text("hello")
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .foregroundColor(.white)
            .background(LinearGradient(gradient: Gradient(colors: [
                Color(red: 0xD0/255.0, green: 0xA0/255.0, blue: 0xC3/255.0),
                Color(red: 0xD9/255.0, green: 0x4E/255.0, blue: 0x42/255.0),
                Color(red: 0xFA/255.0, green: 0xEF/255.0, blue: 0x64/255.0),
            ]),
            startPoint: .topTrailing, endPoint: .bottomLeading))
        }
    }
    

    Image of gradient

  • Opening a URL
    Button("How?") {
        UIApplication.shared.open(URL(string: "https://support.garmin.com/en-US/?faq=lK5FPB9iPF5PXFkIpFlFPA")!)
    }
    
  • Multiple Sheets not working in SwiftUI

    You don’t use multiple sheets … instead use a single sheet with an enum. https://stackoverflow.com/a/63181811/3390

        @State var activeSheet: ActiveSheet? = .showGarminExplanation
        
        enum ActiveSheet: Identifiable {
            case fitbitLogin, showGarminExplanation, showXaiomiExplanation, showOuraExplanation
            
            var id: Int {
                hashValue
            }
        }
    
    ...
            .sheet(item: $activeSheet)  { item in
                switch item {
                case .fitbitLogin:
                    SafariView(url: fitbit.url!)
                case .showGarminExplanation:
                    GarminExplanationView()
                case .showXaiomiExplanation:
                    GarminExplanationView()
                case .showOuraExplanation:
                    GarminExplanationView()
                }
            }
    
    
  • RadioGroupPickerStyle not found

    The reason you can’t find RadioGroupPickerStyle on in your SwiftUI iOS app is that it is only available on macOS

  • SwiftUI navigating
    var body: some View {
        NavigationView {
            VStack {
                Spacer()
                Image("someimage")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                Spacer()
            }
            .navigationBarTitle("Welcome!")
            .navigationBarItems(trailing:
                                    NavigationLink("Next", destination: AnotherView(), isActive: $moveNext)
            )
            .onAppear(perform: {
                DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                    withAnimation {
                        self.moveNext = true
                    }
                }
            })
    
        }
        
    }
    
  • Swift Localization
    1. Open Project properties and under Localizations add French to Base and English-Development Language
    2. Add a new file called Localizable.strings using the New File dialog and filtering to Strings File
    3. Right-click the new file, select Show File Inspector and under Localization check both English and French
    4. Two sub-files under Localizable.strings should have appeared

    To preview a language in SwiftUI

    struct HomeView_Previews: PreviewProvider {
        static var previews: some View {
            HomeView()
                .environment(\.locale, .init(identifier: "fr"))
        }
    }
    
  • Rounding the rectangle of a view background
    VStack {
        Label("text", systemImage: "pills.fill")
            .foregroundColor(.orange)
            .font(.title3)
    }
    .background(RoundedRectangle(cornerRadius: 25.0).foregroundColor(.orange).opacity(0.05))
    
  • Averaging a collection of values in Swift
    let averageMinutesAsleep = itemsToConsider.reduce(0) { $0 + $1.minutesAsleep} / itemsToConsider.count
    
  • Interval in seconds between two dates in Swift
    let seconds = Int(Date().timeIntervalSince(expires))
    
  • Swift date in user defaults
    let expires = defaults.object(forKey: defaultsTokenExpiresName) as? Date
    
  • Switching between open tabs in Xcode

    Next Tab: Command-{ which is Command-Shift-[ Prevous Tab: Command-{ which is Command-Shift-]

  • Using the keychain from Swift

    Based on a blog post I wrote on Sharing tokens between iOS, macOS and watchOS app using the iCloud KeyChain which is I am sure based on something else.

    import os
    import Foundation
    
    class KeyStore {
        func store(name: String, token : String) {
            let data = token.data(using: .utf8)!
            let addquery: [String: Any] = [kSecClass as String: kSecClassGenericPassword as String,
                                           kSecAttrAccount as String: name,
                                           kSecValueData as String: data
            ]
            SecItemDelete(addquery as CFDictionary)
            let status : OSStatus = SecItemAdd(addquery as CFDictionary, nil)
            guard status == errSecSuccess else {
                os_log("store: whoops")
                return
            }
        }
        
        func clear(name: String) {
            let addquery: [String: Any] = [kSecClass as String: kSecClassGenericPassword as String,
                                           kSecAttrAccount as String: name
            ]
            SecItemDelete(addquery as CFDictionary)
        }
        
        func retrieve(name: String) -> String? {
            let getquery: [String: Any] = [kSecClass as String: kSecClassGenericPassword,
                                           kSecAttrAccount as String: name,
                                           kSecReturnData as String: kCFBooleanTrue!,
                                           kSecMatchLimit as String : kSecMatchLimitOne
            ]
            
            var item: CFTypeRef?
            let status = SecItemCopyMatching(getquery as CFDictionary, &item)
            guard status == errSecSuccess else {
                os_log("keyStore.retrieve SecItemCopyMatching error \(status)")
                return nil
            }
            
            guard let data = item as? Data? else {
                os_log("keyStore.retrieve not data")
                return nil
            }
            
            return String(data: data!, encoding: String.Encoding.utf8)
        }
        
    }
    
  • Bolding text in SwiftUI
    Text("normal text ") + Text("bold text").bold() + Text(" normal text") 
    
  • A VS Code Snippet to create Jekyll blog post frontmatter
    "entry": {
        "scope": "markdown",
        "prefix": "en",
        "body": [
            "---",
    "layout: post",
    "title:  \"${1:title}\"",
    "date:   $CURRENT_YEAR-$CURRENT_MONTH-$CURRENT_DATE $CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND +0100",
    "---",
    "${2:body}"
        ],
        "description": "post"
    }
    

    Create/update a root-level settings.json to enable tab-completion:

    {
        "editor.tabCompletion": "on"
    }
    
  • Stopping SwiftUI Text from being clipped
    Text("You've been getting an average of 7 hours and 23 minutes of sleep each night over the last month, which is great.  Keep it up!")
        .fixedSize(horizontal: false, vertical: true)
        .padding()
    

    From Reddit

  • Changing the currently selected SwiftUI TabView tab
    struct OnboardingWizardView: View {
        @State var selectedTab = 0
        var body: some View {
            TabView(selection: $selectedTab) {
                Page1().tag(0)
                Page2().tag(1)
            }.tabViewStyle(PageTabViewStyle())
            .onAppear(perform: {
                DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                    withAnimation {
                        self.selectedTab = 1
                    }
                }
            })
            
        }
    }
    
  • Delaying execution of code in SwiftUI
    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
        self.showProgress = false
    }
    
  • VS Code Mac keyboard shortcut to stage changes

    Ctrl-Shift-G

  • Making a SwiftUI Text fill the frame
    Text("Welcome")
        .font(.largeTitle)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    
  • Resuming Xcode SwiftUI live preview

    To resume Xcode SwiftUI Live Preview press Option-Command-P

  • Royalty free images for use in apps

    Royalty free images for use in apps: https://unsplash.com/

  • Carousel view/multiple pages in SwiftUI

    To create a carousel view in SwiftUI, with multiple pages, use the TabView with .tabViewSyle set to PageTabViewStyle

  • Mac Screenshot

    To open the screenshot utility: Shift Command 5

    Saves to Downloads

  • Getting a # component from a URL

    From https://stackoverflow.com/a/57933248/3390

    func HandleCallback(url: URL) {
        var components = URLComponents()
        components.query = url.fragment
        
        accessToken = components.queryItems?.first(where: { $0.name == "access_token"})?.value
    }
    
  • Formatting and delta dates in Swift
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd"
    let today = dateFormatter.string(from: Date())
    let monthAgo = dateFormatter.string(from: NSCalendar.current.date(byAdding: .month, value: -1, to: NSDate() as Date)!)
    
  • SwiftUI Separator

    The SwiftUI separator is called a divider

  • Formatting numbers in SwiftUI
    Text("\(food.loggedFood.amount, specifier: "%.1f") \(food.loggedFood.unit.plural)")
    
  • A generic Swift REST call/decode function with a type as a parameter

    Pass the type of a generic parameter using T.Type

        var cancellables = [AnyCancellable]()
        
        fileprivate func GetData<T>(url: URL, token: String, resultType: T.Type,  callback: @escaping (T) -> Void) where T:Codable {
            var request = URLRequest(url: url)
            request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
            URLSession.shared.dataTaskPublisher(for: request)
                .tryMap() { element -> Data in
                    // let s = String(data: element.data, encoding: .utf8)
                    // print(s)
                    guard let httpResponse = element.response as? HTTPURLResponse,
                          httpResponse.statusCode == 200 else {
                        throw URLError(.badServerResponse)
                    }
                    return element.data
                }
                .decode(type: resultType, decoder: self.decoder)
                .receive(on: DispatchQueue.main)
                .sink(receiveCompletion: { v in
                    print(v)
                },
                receiveValue: { v in
                    callback(v)
                })
                .store(in: &self.cancellables)
        }
    
  • Widening Jekyll content area

    If your minima Jekyll page width is too narrow, copy the _sass folder from your Jekyll installation to your web site folder, and then edit minima.scss to update the width:

    // Width of the content area
    $content-width:    2048px !default;
    

    You can find out where minima is installed using

    bundle info --path minima
    
  • Launching Jekyll

    When trying to serve your site using jekyll and you get an error like this:

    jekyll serve
    ...
    /Library/Ruby/Gems/2.6.0/gems/bundler-2.1.4/lib/bundler/spec_set.rb:86:in `block in materialize': Could not find public_suffix-4.0.6 in any of the sources (Bundler::GemNotFound)
    

    don’t forget you need to use bundle exec jekyll serve

subscribe via RSS