[M1 Mac, Ventura 13.3.1, Xcode 14.3]
print関数の機能はPythonと同じですが、仕様は全く異なります。
# 変数offsetの出力(バックスラッシュ + 括弧で囲む)
print("offset:\(offset)")
出力例
--------------------
offset:0
[M1 Mac, Ventura 13.3.1, Xcode 14.3]
print関数の機能はPythonと同じですが、仕様は全く異なります。
# 変数offsetの出力(バックスラッシュ + 括弧で囲む)
print("offset:\(offset)")
出力例
--------------------
offset:0
[M1 Mac, Ventura 13.3.1, Xcode 14.3]
指示文が連続する会話形式ではデータをJSONファイルにするなど保存するデータがややこしくなるため、一問一答形式でブラッシュアップしました。
指示文が作成順にうまく並ばないので、EntityのAttributeに生成日時を追加しこれをキーとしてソートさせています。常に先頭の指示文を送信する仕様になっています。
また、指示済かつ回答受信済の内容については送信ボタンを押すとアラートが表示されます。
保存するデータがEntityの形式に縛られるのが何とももどかしいです。JSONファイルとしてiCloudやローカルに出力できないか調べてみます。
これで土台はできあがったので、後はのんびり進めていきます。気が向いたら会話形式にも着手します。
import SwiftUI
import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
entity: Interaction.entity(),
sortDescriptors: [NSSortDescriptor(key: "creationDate", ascending: false)])
private var pairs: FetchedResults<Interaction>
@State var isShowAlert = false
var body: some View {
NavigationView {
VStack{
List {
ForEach(pairs) { pair in
NavigationLink{
if((pair.instruction?.isEmpty) == false){
Draft(text:pair.instruction!, interaction: pair)
}
}
label:{
if((pair.instruction?.isEmpty) == false){
Text(pair.instruction!)
}
}
}
.onDelete(perform: deleteInteraction)
}
.navigationTitle("ChatGPTSwift")
.navigationBarTitleDisplayMode(.inline)
.toolbar{
// 新規作成
ToolbarItem(placement:.navigationBarTrailing){
NavigationLink{
Draft()
}label:{
Text("+")
.font(.system(size: 30))
}
}
}
List {
ForEach(pairs, id: \.self) { pair in
if let res = pair.res , !res.isEmpty {
Text(res)
.foregroundColor(.white)
.background(Color.blue)
}
}
.onDelete(perform: deleteInteraction)
}
Button(action:{
if pairs.first != nil{
let instruction = pairs.first
if instruction!.res == nil{
sendRequest(pairs:pairs)
}else{
isShowAlert = true
print("指示文を入力して下さい")
}
}else{
print("pairsは空です")
}
}){
Text("送信")
.font(.system(size: 24))
}
.alert("指示文を入力して下さい", isPresented: $isShowAlert) {
Button("OK") {
}
} message: {
Text("")
}
}
}
}
<以下略>
[M1 Mac, Ventura 13.3.1, Xcode 14.3]
GPT-3.5のAPIとやりとりできるようになりました。
ChatGPTの力を借り色々試行錯誤しながらで約半日掛かりました。取りあえず送受信を1往復できるようにしています。
チャットのように交互に表示させるのは難しいため、上下段に振り分けました。
次回以降、複数回の送受信に対応させます。
OpenAIがiOS, iPadOSアプリを5/26にリリースしているので、比較しながら機能を増やしていきたいです。
import SwiftUI
import CoreData
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(sortDescriptors:[])
private var pairs: FetchedResults<Interaction>
var body: some View {
NavigationView {
VStack{
List {
ForEach(pairs) { pair in
NavigationLink{
if((pair.instruction?.isEmpty) == false){
Draft(text:pair.instruction!, interaction: pair)
}
}
label:{
if((pair.instruction?.isEmpty) == false){
Text(pair.instruction!)
}
}
}
.onDelete(perform: deleteInteraction)
}
.navigationTitle("ChatGPT リクエスト")
.navigationBarTitleDisplayMode(.inline)
.toolbar{
// 新規リクエスト作成
ToolbarItem(placement:.navigationBarTrailing){
NavigationLink{
Draft()
}label:{
Text("+")
}
}
}
List {
ForEach(pairs, id: \.self) { pair in
if let res = pair.res, !res.isEmpty {
Text(res)
.foregroundColor(.white)
.background(Color.blue)
}
}
.onDelete(perform: deleteInteraction)
}
Button(action:{
if pairs.last != nil{
sendRequest()
}else{
print("pairsは空です")
}
}){
Text("送信")
.font(.system(size: 24))
}
}
}
}
func deleteInteraction(offsets:IndexSet){
for offset in offsets{
viewContext.delete(pairs[offset])
}
do{
try viewContext.save()
}catch{
fatalError("セーブに失敗")
}
}
func sendRequest(){
let urlAPI = "https://api.openai.com/v1/chat/completions";
let apiKey = "API key";
let model = "gpt-3.5-turbo"
let systemStr: String = "あなたは根拠が明確に存在することのみ発言するチャットボットです。"
let authHeader = "Bearer \(apiKey)"
var headers = [String: String]()
headers["Authorization"] = authHeader
headers["Content-Type"] = "application/json"
guard let url = URL(string: urlAPI) else {
fatalError("Invalid URL")
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
// requestData作成・送受信
if let lastInteraction = pairs.last {
if let instruction:String? = lastInteraction.instruction {
print("instruction:")
print(instruction)
let requestData:String = "{\"model\":\"\(model)\", \"messages\":[{\"role\":\"system\",\"content\":\"\(systemStr)\"},{\"role\":\"user\",\"content\":\"\(instruction!)\"}], \"temperature\":0.0}";
print(requestData)
var responseData: [String: Any] = [:]
responseData = sendHTTPRequest(url: url, headers: headers, requestData: requestData)
print(responseData)
if let choices = responseData["choices"] as? [[String: Any]],
let message = choices.first?["message"] as? [String: Any],
var content = message["content"] as? String {
print(content)
lastInteraction.res = content
}
} else {
print("instruction is nil")
return
}
} else {
print("pairs is empty")
return
}
}
func sendHTTPRequest(url: URL, headers: [String: String], requestData: String) -> [String: Any] {
var responseData: [String: Any] = [:]
var timeoutBool = false
let semaphore = DispatchSemaphore(value: 0)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = requestData.data(using: .utf8) // requestDataをData型に変換
let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
if let error = error {
timeoutBool = true
print("HTTP request failed: \(error)")
return
}
if let data = data {
do {
responseData = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:]
} catch {
print("Failed to parse response data: \(error)")
}
}
semaphore.signal()
}
task.resume()
_ = semaphore.wait(timeout: DispatchTime.now() + 90)
if timeoutBool {
<中略>
return [:]
}
return responseData
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
[M1 Mac, Ventura 13.3.1, Xcode 14.3]
前から関数の定義で引数の所にあるアンダースコアが気になっていたので調べてみました。
アンダースコアを付けると関数を使用する際に引数ラベルを省略できるそうです。これには驚きました。普通はアンダースコアなしでも引数ラベルはいらないものですが。
これのおかげでコードの見栄えがどっちにしても悪くなるんですが、Appleにすればどうでもいいようです。
ハードやOSの見た目にはこだわるのに、開発者にだけ見える部分には無頓着ですね。
# 引数にvalueを必ず付ける
func intToString(value: Int) -> String {
return String(value)
}
print(intToString(value:1))
# 引数にvalueを付けなくてもよい
func intToString2(_ value: Int) -> String {
return String(value)
}
print(intToString2(1))
出力
----------
1
1
----------
[M1 Mac, Ventura 13.3.1, Xcode 14.3]
ChatGPTアプリiOS版の製作に着手しました。
取りあえずリクエスト文を入力できるようにしました。メモアプリのコードを一部流用しています。
クリップボードからテキストをペーストできます。macOSなどからOSをまたいでのペーストは次回以降対応するつもりです。
次にこれをAPIに送信してレスポンスを表示するようにします。
2023/7/4追記
TextEditorのタップ時にPasteやSelect Allなどが選択できるため、緑字のペーストボタンは削除しました。
import SwiftUI
struct Draft: View {
@State var text = ""
@FocusState var nameFieldIsForcused: Bool
@Environment(\.managedObjectContext)var viewContext
var interaction: Interaction?
var body: some View {
TextEditor(text:$text)
.frame(minHeight: 0, maxHeight: .infinity) // 行数無制限
.focused($nameFieldIsForcused)
.onAppear{
DispatchQueue.main.asyncAfter(deadline:DispatchTime.now()+0.5){
nameFieldIsForcused = true
}
}
.toolbar{
ToolbarItem(placement:.navigationBarLeading){
if(nameFieldIsForcused){
Button(action:{
pasteText()
}){
Text("ペースト")
.foregroundColor(.green)
}
}
}
ToolbarItem(placement:.navigationBarTrailing){
if(nameFieldIsForcused){
Button(action:{
if interaction != nil{
updateContent(interaction: self.interaction!)
}else{
addContent()
}
}){
Text("完了")
}
}
}
}
}
func updateContent(interaction:Interaction){
interaction.request = text
do{
try viewContext.save()
}catch{
fatalError("セーブに失敗")
}
nameFieldIsForcused = false
}
func addContent(){
let newContent = Interaction(context:viewContext)
newContent.request = text
do{
try viewContext.save()
}catch{
fatalError("セーブに失敗")
}
nameFieldIsForcused = false
}
func pasteText(){
let pasteboard = UIPasteboard.general
if let clipboardString = pasteboard.string {
text += clipboardString
}
}
}
struct Draft_Previews: PreviewProvider {
static var previews: some View {
Draft()
}
}
[M1 Mac, Ventura 13.3.1, Xcode 14.3]
製作中のメモアプリがApple Watchでは読み取りだけだったのを編集できるようにしました。
watchOSではTextEditorは使えないため、代わりにTextFieldを使っています。ただTextfieldでは複数行を扱えないので、全ての行がつながってしまいます。
またApple Watch Series 7/8/Ultra以外はキーボード入力ではなく音声入力になります。
発売から8年が経過しているというのに未だにTextEditorが使えないとは。Vision ProだけではなくwatchOSにも力を注いで欲しいです。今のところ、自分で代替の構造体を作るしかないようです。
GoogleのWearOSがどのような内容なのか気になります。そろそろFlutterでGoogle Pixel Watch用アプリも作りたくなってきました。
import SwiftUI
struct DraftAppleWatch: View {
@State var text = ""
@FocusState var nameFieldIsForcused: Bool
@Environment(\.managedObjectContext)var viewContext
var note: Note?
var body: some View {
VStack {
TextField("", text: $text)
.focused($nameFieldIsForcused)
.onAppear{
DispatchQueue.main.asyncAfter(deadline:DispatchTime.now()+0.5){
nameFieldIsForcused = true
}
}
Button(action:{
if note != nil{
updateContent(note: self.note!) // メモ編集
}else{
addContent() // 新規メモ追加
}
}){
Text("完了")
}
}
}
func updateContent(note:Note){
note.content=text
do{
try viewContext.save()
}catch{
fatalError("セーブに失敗")
}
nameFieldIsForcused = false
}
func addContent(){
let newContent = Note(context:viewContext)
newContent.content = text
do{
try viewContext.save()
}catch{
fatalError("セーブに失敗")
}
nameFieldIsForcused = false
}
}
struct DraftAppleWatch_Previews: PreviewProvider {
static var previews: some View {
DraftAppleWatch()
}
}
[M1 Mac, Ventura 13.3.1, Xcode 14.3]
iOSとwatchOSをそれぞれ16.5.1、9.5.2にアップデートしたところWatch Appが消えてしまいました。Watch App with iOS Appの方は残っています。
自製Watch Appを全て消して再インストールしようとするとApple Watchを認識できないとか色々難癖を付けてきます。
結局Mac、iPhone、Apple Watchを全て再起動すると直りました。最初からそう言ってほしいものです。
この手のトラブルは以前から発生しているようでApple開発者フォーラムでも非難囂々です。ちなみにMacとiPhoneはUSBハブ経由ではなく直接つなぐ方がいいらしいです。
[M1 Mac, Ventura 13.3.1, Xcode 14.3]
改良カレンダーをタップした時の表示に元号を追加しました。フォントサイズを大きくすると赤色でも問題なく読めます。
今回は日本固有の内容だったためかChatGPTの回答がかなり怪しく、大幅な修正を加えました。
import SwiftUI
struct ContentView: View {
let date: Date
var body: some View {
VStack (spacing: 15){
Text(getFormattedDate())
.font(.system(size: 30))
.foregroundColor(.green)
Text(getFormattedYear())
.font(.system(size: 30))
.foregroundColor(.red)
Text(getFormattedWeekday() + "曜日")
.font(.system(size: 30))
.foregroundColor(.yellow)
Text(getFormattedTime())
.font(.system(size: 30))
.foregroundColor(.blue)
}
.padding()
}
func getFormattedDate() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd"
return dateFormatter.string(from: date)
}
func getFormattedYear() -> String {
let calendar = Calendar(identifier: .japanese)
let year = calendar.component(.year, from: date)
return "令和" + String(year) + "年"
}
func getFormattedWeekday() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "E"
dateFormatter.locale = Locale(identifier: "ja_JP")
return dateFormatter.string(from: date)
}
func getFormattedTime() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
return dateFormatter.string(from: date)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(date: Date())
}
}
[M1 Mac, Ventura 13.3.1, Xcode 14.3]
Complicationの改良カレンダーですが、アナログ時計のため時針や分針が重なると読みにくくなります。
そこでタップすると日時を表示するようにしました。デジタル時計は止まったまま動きません。
import SwiftUI
struct ContentView: View {
let date: Date
var body: some View {
VStack (spacing: 20){
Text(getFormattedDate())
.font(.system(size: 30))
.foregroundColor(.green)
Text(getFormattedWeekday() + "曜日")
.font(.system(size: 30))
.foregroundColor(.yellow)
Text(getFormattedTime())
.font(.system(size: 30))
.foregroundColor(.blue)
}
.padding()
}
func getFormattedDate() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy/MM/dd"
return dateFormatter.string(from: date)
}
func getFormattedWeekday() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "E"
dateFormatter.locale = Locale(identifier: "ja_JP")
return dateFormatter.string(from: date)
}
func getFormattedTime() -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
return dateFormatter.string(from: date)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(date: Date())
}
}
[M1 Mac, Ventura 13.3.1, Xcode 14.3]
改良カレンダーのフォントサイズを調整して3行にし、月表示を追加しました。
ChatGPTが考えたコードを微修正しています。
struct DateToolComplicationEntryView : View {
var entry: Provider.Entry
var body: some View {
VStack (spacing: -5){
Text(getWeekday(entry.date))
.font(.system(size: 18))
.foregroundColor(.yellow)
Text(getMonth(entry.date))
.font(.system(size: 18))
.foregroundColor(.green)
Text(getDay(entry.date))
.font(.system(size: 20))
.foregroundColor(.white)
}
}
func getWeekday(_ date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "E"
dateFormatter.locale = Locale(identifier: "ja_JP")
return dateFormatter.string(from: date)
}
func getMonth(_ date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "M"
return dateFormatter.string(from: date)
}
func getDay(_ date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "d"
return dateFormatter.string(from: date)
}
}