3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

レコチョクAdvent Calendar 2023

Day 14

【iOS】ライトニングトークイベントでAI関連の発表をした振り返り

Last updated at Posted at 2023-12-13

この記事はレコチョク Advent Calendar 2023の14日目の記事となります。

はじめに

こんにちは。株式会社レコチョクの長島です。
2022年4月に新卒で入社し、iOSアプリの開発をしています。最近はソーシャルゲーム『ブルーアーカイブ』のサントラ『Blue Archive Original Soundtrack Vol.4 ~Aiming for the ideal freedom~』をよく聞いています。
Unwelcome Schoolはミームの曲として有名ですが、元々すごく良い曲なのでオススメです。仕事が大変なときによく聞いています。

今年はiOSアプリの開発に携わると同時に、AIに関する勉強会などにも多く参加していました。レコチョクでは、音楽市場への新たな価値提供の実現を目的としたwith AI プロジェクトが発足しており、RecoChat with AIという全社員が業務に活用できるAIチャットツールが提供されていたり、希望者は検証用としてChatGPT PlusやGitHub Copilot for Businessを利用可能となっていたりします。

私自身もAIへの取り組みとして、以前【ChatGPT】ChatGPTでChatGPTチャットボット試作という記事を執筆して公開しました。今回は社内のライトニングトークイベントで3回ほど発表したので、その内容を紹介します。

発表したもの

AIとラバーダッキング

20230808_wAIナガシマ_AIとラバーダッキング-01.png

「AIとラバーダッキングぐらいの気持ちで話してみない?」という発表です。

「AIに何を聞けばいいのかわからない」「AIに関する単語の理解が難しい」という課題があったので、その解決のためにAIを相手としたラバーダッキング法をおすすめしました。

ラバーダッキング法は、プログラマがアヒルのおもちゃに問題を説明することで、その問題への解決策を思いつくことがあったり、問題への理解を深めたりすることができる手法となっています。
このアヒルのおもちゃの部分をAIに置き換えることで、気楽にAIを使うことができます。また、アイディアも出してもらえるというメリットも得られるため、最高のラバーダッキング相手になるのではないかと考えました。

20230808_wAIナガシマ_AIとラバーダッキング-16.png

自分はこの形で、開発中に起きたエラー文をAIと一緒に読んだり、AIが出してきたアイディアの内容に関して掘り下げたりして問題を解決しており、開発効率が大幅に向上したと感じています。

Copilot for Xcodeに触れてみた

20230929_wAIナガシマ_CoPilot For Xcode-01.png

Xcode上でCopilotを利用するツールについての発表です。

GitHub CopilotはXcode向けには公式のプラグインがリリースされておらず、非公式のプラグインに頼る必要があります。今回は、その中でももっとも開発が進んでいると思われるCopilot for Xcodeというものを用いました。
これは、Copilotによるコード推測機能に加え、GPTモデルとチャットできる機能などが実装されています。

Copilot and Chat Feature.png

今回はCoplitのコード補完機能を活用しながら、ためしに以下の要素を持つ簡単な画面の作成を試みました。

  • 事前に用意した画像を表示する
  • 「Hello, world!」の文字列を表示する
  • FizzBuzz問題を扱えるコードと、その結果の文字列を表示する

その結果として、30秒程度でまっさらな状態から以下のコードが書けました。

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image("copilot")
                .resizable()
                .scaledToFit()
                .frame(width: 200, height: 200)
            Text("Hello, world!")
                .padding()
            Text(fizzBuzz(15).joined(separator: ", "))
                .padding()
                .multilineTextAlignment(.center)
        }
    }
    
    func fizzBuzz(_ n: Int) -> [String] {
        var result = [String]()
        for i in 1...n {
            if i % 15 == 0 {
                result.append("FizzBuzz")
            } else if i % 3 == 0 {
                result.append("Fizz")
            } else if i % 5 == 0 {
                result.append("Buzz")
            } else {
                result.append(String(i))
            }
        }
        return result
    }
}

#Preview {
    ContentView()
}

簡単なロジックと画面ですが、これが30秒という短い時間で実装できることが驚きでした。Copilot for Xcodeには自分が利用した範囲で以下のような利点がありました。

  • 素早いコード推論機能
  • 推論結果がわかりやすいポップアップウィンドウ
  • その場でコードについて聞けるチャット機能

今後は業務上のコードの記載や、テストコードの生成の手助けなどの多くの場面で活用できそうです。

音楽プレイヤーを作ってもらおう

20231117_wAIナガシマ_GPT4Analysis-01.png

GPT-4のAnalytics機能を用いて画像を読み込んでもらい、音楽プレイヤーのUIを作ってもらおうとした発表です。
自社で開発しているPlayPASSアプリのスクリーンショットを添付し、ChatGPTに対して指示しました。

これはiOSの音楽プレイヤーの画面です。画面構成を再現し、プレイヤーが機能する形でSwiftUIでコードを書いてください。

ChatGPTはこのスクリーンショットを読み取り、以下の画像右のUIを作成できるコードが生成されました。画像左のスクショと見比べると、ボタンの配置・大きさなどをある程度再現できていることが分かりました。

20231117_wAIナガシマ_GPT4Analysis-33のコピー.png

ChatGPTの生成したコードは以下の形となります。
AVPlayerを利用し、ローカルファイルを実際に再生する機能がついているところに驚きました。

ChatGPTの生成したコード
//
//  MusicPlayerView.swift
//  SwiftUIGPTTest
//
//  Created by 長島 大和 on 2023/11/01.
//

import SwiftUI
import AVKit

struct MusicPlayerView: View {
    @StateObject private var player = AudioPlayer()
    @State private var isPlaying = false
    @State private var progress = 0.0
    @State private var currentTime = 0.0
    @State private var duration = 0.0
    
    var body: some View {
            NavigationView {
                VStack {
                    Image(systemName: "photo")  // ジャケット画像
                        .resizable()
                        .scaledToFit()
                        .frame(width: 300, height: 300)
                        .cornerRadius(20)
                        .padding(.top, 50)
                    
                    Text("Song Title")
                        .font(.title)
                        .fontWeight(.bold)
                        .padding(.top, 20)
                    
                    Text("Artist Name")
                        .font(.headline)
                        .foregroundColor(.secondary)
                        .padding(.top, 5)
                    
                    Slider(value: $progress, in: 0...1, onEditingChanged: { editing in
                        if !editing {
                            player.seek(to: duration * progress)
                        }
                    })
                    .padding(.horizontal, 30)
                    .padding(.top, 20)
                    
                    HStack {
                        Text(formatTime(currentTime))
                        Spacer()
                        Text(formatTime(duration - currentTime))
                    }
                    .padding(.horizontal, 30)
                    .padding(.top, 5)
                    
                    HStack {
                        Spacer()
                        Button(action: {
                            player.previous()
                        }) {
                            Image(systemName: "backward.end.fill")
                                .font(.largeTitle)
                        }
                        Spacer()
                        Button(action: {
                            if isPlaying {
                                player.pause()
                            } else {
                                player.play()
                            }
                            isPlaying.toggle()
                        }) {
                            Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill")
                                .font(.system(size: 60))
                        }
                        Spacer()
                        Button(action: {
                            player.next()
                        }) {
                            Image(systemName: "forward.end.fill")
                                .font(.largeTitle)
                        }
                        Spacer()
                    }
                    .padding(.top, 20)
                    
                    Spacer()
                    
                    HStack {
                        Spacer()
                        Button(action: {
                            // プレイリストのアクションをここに追加
                        }) {
                            Image(systemName: "text.badge.plus")
                                .font(.title)
                        }
                        Spacer()
                        Button(action: {
                            // シャッフルのアクションをここに追加
                        }) {
                            Image(systemName: "shuffle")
                                .font(.title)
                        }
                        Spacer()
                        Button(action: {
                            // リピートのアクションをここに追加
                        }) {
                            Image(systemName: "repeat")
                                .font(.title)
                        }
                        Spacer()
                        Button(action: {
                            // ライブラリのアクションをここに追加
                        }) {
                            Image(systemName: "music.note.list")
                                .font(.title)
                        }
                        Spacer()
                    }
                    .padding(.top, 30)
                    .padding(.bottom, 30)
                }
                .navigationBarTitle("Now Playing", displayMode: .inline)
                .onAppear {
                    player.load("sample_audio")
                }
            }
            .onReceive(player.timer) { _ in
                self.currentTime = player.currentTime
                self.duration = player.duration
                self.progress = player.currentTime / player.duration
            }
        }
    
    func formatTime(_ time: Double) -> String {
        guard time >= 0 else { return "00:00" }
        let minutes = Int(time) / 60
        let seconds = Int(time) % 60
        let milliseconds = Int((time - floor(time)) * 100)
        return String(format: "%02d:%02d:%02d", minutes, seconds, milliseconds)
    }

}

class AudioPlayer: ObservableObject {
    private var player: AVPlayer?
    private var timeObserverToken: Any?
    let timer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
    
    var currentTime: Double {
        return player?.currentTime().seconds ?? 0
    }
    
    var duration: Double {
        return player?.currentItem?.duration.seconds ?? 0
    }
    
    func load(_ resourceName: String) {
        if let url = Bundle.main.url(forResource: resourceName, withExtension: "mp3") {
            player = AVPlayer(url: url)
            addPeriodicTimeObserver()
        }
    }
    
    func play() {
        player?.play()
    }
    
    func pause() {
        player?.pause()
    }
    
    func previous() {
        // シンプルな実装として、曲の最初にシークします
        seek(to: 0)
    }
    
    func next() {
        // シンプルな実装として、曲の最初にシークします
        seek(to: 0)
    }
    
    func seek(to time: Double) {
        let targetTime = CMTime(seconds: time, preferredTimescale: 600)
        player?.seek(to: targetTime)
    }

    func seekToProgress(to progress: Double) {
        let targetTime = CMTime(seconds: duration * progress, preferredTimescale: 600)
        player?.seek(to: targetTime)
    }
    
    private func addPeriodicTimeObserver() {
        // Ensure the player is not nil
        guard let player = player else { return }
        
        // Create time observer
        let time = CMTime(seconds: 1, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        timeObserverToken = player.addPeriodicTimeObserver(forInterval: time, queue: .main) { [weak self] time in
            self?.objectWillChange.send()
        }
    }
}

struct MusicPlayerView_Previews: PreviewProvider {
    static var previews: some View {
        MusicPlayerView()
    }
}

この生成されたコードを製品に直接組み込むことはリスクが伴う可能性がありますが、たとえば初期のプロトタイピングや、画面デザインからコードを作成する第一歩としては十分だと感じられました。

また、(面白そうだったので)以下のすべての音楽プレイヤーの画像のスクリーンショットを撮影し、「これらすべてを組み合わせて最強の音楽プレイヤーを作ってください」とChatGPTに指示してみました。

  • Apple Music
  • Amazon Music
  • Spotify
  • YouTube Music
  • Eggs
  • PlayPASS

結果は以下の通りです。もっとプロンプトを詰めないと、こういう無茶振りはあまり上手くいかなさそうですね。

まとめ

以下の3つのタイトルでライトニングトークをしました。

  • AIとラバーダッキング
  • Copilot for Xcodeに触れてみた
  • 音楽プレイヤーを作ってもらおう

これらの発表を通じて、AIが身近にあるツールだと認識できました。Copilot for Xcodeを便利なコード補完機能として活用したり、Analytics機能を用いて画面の制約について相談したりと、AIを利用した開発が続けられています。

直近でもGPTsやAssistance APIなど画期的な新機能が追加されている現状、キャッチアップするのは大変ではあります。しかしそこに楽しさも感じているので、来年はより面白そうな部分や実用的な深い部分に目を向けつつ、継続してAIに触れていこうと思います。

明日のレコチョク Advent Calendar 2023は15日目「音楽アプリで使えるCarPlayのUIについて」です。お楽しみに!


この記事はレコチョクのエンジニアブログの記事を転載したものとなります。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?