How I build a meditation app in a week

How I build a meditation app in a week

December 25, 2023
Programming, Meditation

Prelude #

The story was I built an iOS app for group meditation. It works like Clubhouse but for regular meditators instead. The instructor can remotely sit with students, who can reserve class and book 1:1 sessions. Teachers can record sessions and offer the replay option for students. During meditation, they can play ambient music along with guidance.

I built the app in a week. I have to say that Swift is a much more enjoyable language than Objective-C which I developed apps on years ago. Here is a high level overview of the short story. You can find the complete code on Github.

My apologies that I had little time to write more documentation for it. Fortunately Swift is expressive enough and I had made considerable efforts making my code as readable as possible.

Streaming #

I use Agora as the audio streaming vendor (like Clubhouse). The streaming service was wrapped into a class. The reason is mostly for compatibility with Agora’s older SDKs. Also this simplified referencing and potential inheritance. The class structure appears as follows:

class Broadcaster: NSObject, AgoraRtcEngineDelegate, ObservableObject {
    var channelName: String
    var uid: UInt
    var role: AgoraClientRole
    var recordingConfig: AgoraAudioRecordingConfiguration?
    @State private var cancellables = Set<AnyCancellable>()
    @State private var initialized: Bool = false
    @State private var joined: Bool = false
    private(set) var token: String?
    private var bags = Set<AnyCancellable>()
    var agoraKit: AgoraRtcEngineKit!

    var connectionState: AgoraConnectionState {
        agoraKit.getConnectionState()
    }

    init(channelName: String, role: AgoraClientRole, uid: UInt) {
        self.channelName = channelName
        self.role = role
        self.uid = uid
        super.init()
    }

Network Service #

Swift code for networking is almost boilerplate thanks to Swift’s versatile and modern language design.

struct NetworkService {
    let baseURL: String
    private func getToken() -> String? {
        return UserDefaults.standard.object(forKey: "token") as? String
    }

    func get<U>(from: String) -> AnyPublisher<U, Error> where U: Decodable {
        let url = URL(string: baseURL + from)!
        var request = URLRequest(url: url)
        request.httpMethod = "GET"
        if getToken() != nil {
            request.setValue("Bearer \(getToken()!)", forHTTPHeaderField: "Authorization")
        }
        return run(request)
    }

    func post<T, U>(_ entry: T, to: String) -> AnyPublisher<U, Error>
        where T: Encodable, U: Decodable
    {
        let url = URL(string: baseURL + to)!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        if getToken() != nil {
            request.setValue("Bearer \(getToken()!)", forHTTPHeaderField: "Authorization")
        }
        let encoder = JSONEncoder()
        let jsonData = try! encoder.encode(entry)
        request.httpBody = jsonData
        return run(request)
    }

    func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<T, Error> {
        let decoder = JSONDecoder()
        var result: AnyPublisher<T,Error>
        do {
            result = URLSession.shared
                .dataTaskPublisher(for: request)
                .map { $0.data }
                .handleEvents(receiveOutput: { print("<<< Data received:\n", NSString(
                    data: $0,
                    encoding: String.Encoding.utf8.rawValue
                )!) })
                .decode(type: T.self, decoder: decoder)
                .receive(on: DispatchQueue.main)
                .eraseToAnyPublisher()
        }
        return result
    }
}

Syncing heartbeat with servers using websocket is also straightforward:

func sendHeartbeat() {
    let message = URLSessionWebSocketTask.Message.string("heartbeat")
    webSocketTask?.send(message) { error in
        if let error = error {
            print("Error sending heartbeat: \(error)")
        }
    }
}

UI #

SwiftUI is declarative, almost like CSS, which quite pleasantly surprised me. I was still traumatized by Obj-C UI experience. This is even more sweetened by extension syntax to separate UI from event handling and state transitions. You can write those code in a breeze:

extension MusicListView {

    func play(music: Payload.Music?) {
        guard let music = music, let url = URL(string: music.url) else { return }

        if music == musicInPlay, let player = audioPlayer, !isPlaying {
            player.play()
        } else {
            stop()
            let playerItem = AVPlayerItem(url: url)
            audioPlayer = AVPlayer(playerItem: playerItem)

            // Resume playback from the stored played time
            if let storedProgress = playbackProgress[music] {
                audioPlayer?.seek(to: CMTime(
                    seconds: storedProgress.played,
                    preferredTimescale: 1
                ))
            }
            audioPlayer?.play()
            musicInPlay = music
        }
        isPlaying = true
    }

    func stop() {
        audioPlayer?.pause()
        audioPlayer = nil
        musicInPlay = nil
        isPlaying = false
    }

    func pause() {
        audioPlayer?.pause()
        isPlaying = false
    }

Reflections #

  • You might well want to work with Swift, but not Apple. Maybe Rust offers such circumvention with more powerful browser support and embedded wasm code.
  • The paradigm on mobile dev today has very closely resembled front-end development. I was constantly reminded of react/redux, and such.
  • Please let me know if you find this app useful.