[Swift] 20 メモアプリ製作 その6 TextEditorの行数を無制限にする

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

macOS版にメモを10行より多く書くとアプリが落ちてしまいました。

行数を無制限にするには以下のようにframeというビュー修飾子で設定します。

import SwiftUI

struct Draft: View {
    @State var text = ""
    @FocusState var nameFieldIsForcused: Bool
    @Environment(\.managedObjectContext)var viewContext
    var note: Note?

    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:.navigationBarTrailing){
                    if(nameFieldIsForcused){
                        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 Draft_Previews: PreviewProvider {
    static var previews: some View {
        Draft()
    }
}

[Swift] 19 メモアプリ製作 その5 iPadOS, macOS対応

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

macOSとiPadOSでもビルドし、4つのApple製OSでメモアプリを使えるようにしました。CloudKitによりデータをiCloudで共有しています。

iOS、iPadOS、macOSではメモ表示・編集、watchOSではメモ表示ができます。

macOSを対象にビルドしたアプリは階層の深いところにあるため、起動後Dockに残してからFinderに表示させ、アプリケーションへコピーしました。

これで当初の目的はほぼ達成しました。ただし前にも書いたようにApple Developer登録していないとこのアプリ(のCloudKit)は使えないので、他社の安価なバックエンドサービスが使えないか調査します。

iPadOS版
macOS版

[Swift] 18 メモアプリ製作 その4 Apple Watch対応

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

作成したメモアプリをApple Watchでも使えるようにしました。

最初からWatch App with iOS Appとしてプロジェクトを作成すると何故かCloudKitとの連携ができなかったので、まずiOS Appとしてプロジェクトを作成しCloudKit連携させてからTargetにWatch Appを追加しました。

ただこの方法ではWatch Appの名前をiOS Appと同じにはできません。設定した名前の末尾に”Watch App”が追加されるので同じにしても問題はないはずなのに融通が利かないです。

これまではNotebookというメモアプリを使っていたのですが、Apple Watchに表示させるとメモ内容が薄くグレーアウトしていて見にくい仕様になっています。対して今回のアプリはグレーアウトもなくフォントサイズも大きめでだいぶ見やすくなりました。

XcodeのUIといいSwiftの言語仕様といい、相変わらずの使いにくさで愛着が生まれそうにありません。特にforEach文のinの後に置くべき反復変数を省略できる、というまぎらわしい仕様にはあきれました。C#のようにforeach(int num in numbers)にして欲しいです。inの後に続く単語が何も関係ないと知り、少しキレそうになりました。Flutterで作ったらこのようなストレスとは無縁になるのでしょうか。

次はコンプリケーションに対応させて、文字盤からアプリを呼び出せるようにします。

watchOS版 実機
iOS版
import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext)var viewContext

    @FetchRequest(sortDescriptors:[])
    var contents: FetchedResults<Note>

    var body: some View {
        NavigationView{
            List{
                ForEach(contents){content in
                    NavigationLink{
                        DraftAppleWatch()
                    }label:{
                        Text(content.content!)
                    }
                }
                .onDelete(perform:deleteContent)
            }
            .navigationTitle("メモリスト")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
    
    func deleteContent(offsets:IndexSet){
        for offset in offsets{
            viewContext.delete(contents[offset])
        }
            
        do{
            try viewContext.save()
        }catch{
            fatalError("セーブに失敗")
        }
    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

[Swift] 17 メモアプリ製作 その3 CloudKit実装

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

前回の記事で紹介したAmazonの電子書籍を読了しました(Kindle Unlimited)。

まだ不完全ながらメモアプリが一応完成しました。

あとは独力で機能を充実させていきます。

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext)var viewContext

    @FetchRequest(sortDescriptors:[])
    var contents: FetchedResults<Note>

    var body: some View {
        NavigationView{
            List{
                ForEach(contents){content in
                    NavigationLink{
                        if((content.content?.isEmpty) == false){
                            if((content.content?.isEmpty) == false){
                                Draft(text:content.content!, note: content)
                            }
                        }
                    }label:{
                        if((content.content?.isEmpty) == false){
                            Text(content.content!)
                        }
                    }
                }
                .onDelete(perform:deleteContent)
            }
            .navigationTitle("リスト")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar{
                ToolbarItem(placement:.navigationBarLeading){
                    EditButton()
                }
                ToolbarItem(placement:.navigationBarTrailing){
                    NavigationLink{
                        Draft()
                    }label:{
                        Text("+")
                    }
                }
            }

        }
    }
    func deleteContent(offsets:IndexSet){
        for offset in offsets{
            viewContext.delete(contents[offset])
        }
            
        do{
            try viewContext.save()
        }catch{
            fatalError("セーブに失敗")
        }
    }

}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

[Swift] 16 メモアプリ製作 その2 CloudKit

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

CoreDataやCloudKitが上手く使えず途方に暮れていたところ、Amazonに個人出版のいい電子書籍を見つけました。

参考書籍

ただCloudKitはApple Developer登録しないと使えません(12,980円/年)。とりあえず半年ぶりに登録しましたが、iOS, iPadOS, watchOS, macOSのクロスプラットフォームなメモアプリを作ったところで、登録が切れると使えなくなります。これには最初の意気込みもトーンダウンです。

このシリーズ記事はそれなりに長く続けるつもりでしたが、あと数回で最終回になるかもしれません。AWSなど他社のバックエンドサービスが安価で使えないか、調査を進めていきます。

ところで、Swiftの命名規則はキャメルケース(単語間を直接つなげる)です。さすがに4単語、5単語連結になってくると可読性が著しく低下しますね。まあこれも慣れでしょうか。

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView{
            List{
                Text("Hello,world!")
                Text("Hello,world!")
            }
            .navigationTitle("リスト")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar{
                ToolbarItem(placement:.navigationBarLeading){
                    EditButton()
                }
                ToolbarItem(placement:.navigationBarTrailing){
                    NavigationLink{
                        Draft()
                    }label:{
                        Text("+")
                    }
                }
            }

        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

[Swift] 15 メモアプリ製作 その1

[M1 Mac, Ventura 13.3.1, Xcode 14.3]

iOS, watchOS, iPadOS, macOSで使えるメモアプリの製作に取り掛かりました。

とりあえず叩き台のコードをChatGPTに作成してもらいました。

あっさり作ってくれてちょっと引いています。

非IDEにこだわらなければ、Xcodeにどっぷり依存で簡単なアプリを量産できそうです。

import SwiftUI

struct Memo: Identifiable {
    let id = UUID()
    let title: String
    let content: String
}

struct MemoListView: View {
    @State var memos: [Memo] = [
        Memo(title: "メモ1", content: "これはメモ1です。"),
        Memo(title: "メモ2", content: "これはメモ2です。"),
        Memo(title: "メモ3", content: "これはメモ3です。")
    ]
    
    var body: some View {
        NavigationView {
            List(memos) { memo in
                NavigationLink(destination: MemoDetailView(memo: memo)) {
                    Text(memo.title)
                }
            }
            .navigationBarTitle("メモ一覧")
            .navigationBarItems(trailing: NavigationLink(destination: MemoEditView(memos: $memos)) {
                Image(systemName: "plus")
            })
        }
    }
}

struct MemoDetailView: View {
    let memo: Memo
    
    var body: some View {
        VStack {
            Text(memo.title)
                .font(.title)
            Text(memo.content)
                .padding()
            Spacer()
        }
        .navigationBarTitle(memo.title)
    }
}

struct MemoEditView: View {
    @Binding var memos: [Memo]
    @State var title: String = ""
    @State var content: String = ""
    
    var body: some View {
        Form {
            Section(header: Text("タイトル")) {
                TextField("タイトルを入力してください", text: $title)
            }
            Section(header: Text("内容")) {
                TextEditor(text: $content)
            }
            Section {
                Button(action: {
                    let memo = Memo(title: title, content: content)
                    memos.append(memo)
                }) {
                    Text("保存")
                }
            }
        }
        .navigationBarTitle("新規メモ")
    }
}

[Swift] 14 ファイルのUniform Type Identifier(UTI)を取得

[M1 Mac, Big Sur 11.6.8, Swift 5.5.2, NO IDE]

久々のSwiftネタです。

ファイルのUTIを取得するコードを紹介します。Core Foundationを使用します。コードは参考サイトから拝借しました。

printutiファイルは新設した/usr/local/bin2に置き、パスを通して使えるようにしました。

C++でも出来なくはないみたいですが、かなりややこしそうなので素直にSwiftを使う方がいいようです。

Swiftコードをライブラリ化してC++で使いたいものの、さくっと調べた感じでは情報はありませんでした。

import Foundation

if let argument = CommandLine.arguments.dropFirst().first {
    let url = URL(fileURLWithPath: argument)
    if let uti = (try? url.resourceValues(forKeys: [.typeIdentifierKey]))?.typeIdentifier {
        print(uti)
    }
    else {
        print("UTI取得に失敗しました: \(argument)")
    }
}
// 実行ファイル作成(ファイル名はprintuti)
swiftc -o printuti main.swift

// UTIの取得
printuti [filepath]

// 使用例
~ $ printuti test.png 
public.png

参考サイト

[Swift] 13 自製ヘルスケアapp / UIKit DatePicker

UIDatePickerを配置しました。他の機種にも対応できるようウィジェットの座標・サイズは画面サイズに対する比率で算出しています。

次回はボタンの形などを設定します。

import UIKit

extension UIColor {
    static let blackRGB = UIColor(red: 0/255, green: 0/255, blue: 0/255, alpha: 1)
}

class ViewController: UIViewController {
    let appName = UILabel()
    let datepicker_from_lbl = UILabel()
    let datepicker_from = UIDatePicker()
    let datepicker_to_lbl = UILabel()
    let datepicker_to = UIDatePicker()
    let button_HR = UIButton()
    let button_HRV = UIButton()
    
    let width = Float(UIScreen.main.bounds.size.width)
    let height = Float(UIScreen.main.bounds.size.height)

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.white
        
        appName.text = "Health Manager"
        appName.textAlignment = .center
        appName.textColor = UIColor.blackRGB
        appName.font = UIFont.systemFont(ofSize: 40)
        view.addSubview(appName)
        
        datepicker_from_lbl.text = "From"
        datepicker_from_lbl.textColor = UIColor.blackRGB
        datepicker_from_lbl.font = UIFont.systemFont(ofSize: 24)
        view.addSubview(datepicker_from_lbl)
        
        datepicker_to_lbl.text = "To"
        datepicker_to_lbl.textColor = UIColor.blackRGB
        datepicker_to_lbl.font = UIFont.systemFont(ofSize: 24)
        view.addSubview(datepicker_to_lbl)
        
        datepicker_from.preferredDatePickerStyle = .compact
        datepicker_from.datePickerMode = .date
        view.addSubview(datepicker_from)
        
        datepicker_to.preferredDatePickerStyle = .compact
        datepicker_to.datePickerMode = .date
        view.addSubview(datepicker_to)
        
        button_HR.setTitle("心拍数", for: .normal)
        button_HR.setTitleColor(UIColor.blue, for: .normal)
        view.addSubview(button_HR)
        
        button_HRV.setTitle("心拍変動", for: .normal)
        button_HRV.setTitleColor(UIColor.blue, for: .normal)
        view.addSubview(button_HRV)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        appName.sizeToFit()
        let AN_width = CGFloat(width*2/3)
        let AN_height = CGFloat(height*0.1)
        let AN_x = (width - Float(AN_width))/2
        appName.frame = CGRect.init(
            x: CGFloat(AN_x),
            y: CGFloat(height*0.15),
            width: AN_width,
            height: AN_height)

        let DP_width = CGFloat(width*0.2)
        let DP_height = CGFloat(height*0.1)
        let DP_x = width/2
        datepicker_from.frame = CGRect.init(
            x: CGFloat(DP_x),
            y: CGFloat(height*0.25),
            width: DP_width,
            height: DP_height)
        
        let DPL_width = CGFloat(width*0.15)
        let DPL_height = CGFloat(height*0.1)
        let DPL_x = DP_x - width*0.15
        datepicker_from_lbl.frame = CGRect.init(
            x: CGFloat(DPL_x),
            y: CGFloat(height*0.25),
            width: DPL_width,
            height: DPL_height)
        
        datepicker_to.frame = CGRect.init(
            x: CGFloat(DP_x),
            y: CGFloat(height*0.32),
            width: DP_width,
            height: DP_height)
        
        datepicker_to_lbl.frame = CGRect.init(
            x: CGFloat(DPL_x),
            y: CGFloat(height*0.32),
            width: DPL_width,
            height: DPL_height)

        let BT_width = CGFloat(width*0.2)
        let BT_height = CGFloat(height*0.1)
        let BT_x = (width - Float(BT_width))/2
        button_HR.frame = CGRect.init(
            x: CGFloat(BT_x),
            y: CGFloat(height*0.40),
            width: BT_width,
            height: BT_height)
        
        button_HRV.sizeToFit()
        button_HRV.frame = CGRect.init(
            x: CGFloat(BT_x),
            y: CGFloat(height*0.48),
            width: BT_width,
            height: BT_height)

    }
}

[Swift] 12 自製ヘルスケアapp / UIKit

コードだけでGUIを作成するため、SwiftUIでの開発を中断しUIKitに乗り換えました。

JavaやC++ではフレームワークにとらわれずコードだけで好きなようにGUIアプリを作成していたのが、SwiftUIですっかり調子を狂わされてしまいました。

ようやく自分の土俵で取り組めそうですが、Javaのように公式ドキュメントがしっかりしているわけではないので前途多難は続くでしょう。

import UIKit

extension UIColor {
    static let blackRGB = UIColor(red: 0/255, green: 0/255, blue: 0/255, alpha: 1)
}

class ViewController: UIViewController {
    var label = UILabel()
    var button_HR = UIButton()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = UIColor.white
        
        label.text = "Health Manager"
        label.textAlignment = .center
        label.textColor = UIColor.blackRGB
        label.font = UIFont.systemFont(ofSize: 40)
        view.addSubview(label)
        
        button_HR.setTitle("心拍数", for: .normal)
        button_HR.setTitleColor(UIColor.red, for: .normal)
        view.addSubview(button_HR)
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        let width = Float(UIScreen.main.bounds.size.width)
        
        label.sizeToFit()
        let widthGap = (width - Float(label.frame.width)) / 2
        label.frame = CGRect.init(x: CGFloat(widthGap),
            y: 200,
            width: label.frame.width,
            height: label.frame.height)
        
        button_HR.sizeToFit()
        let widthGap_btn = (width - Float(button_HR.frame.width)) / 2
        button_HR.frame = CGRect.init(x: CGFloat(widthGap_btn),
            y: 300,
            width: button_HR.frame.width,
            height: button_HR.frame.height)

    }
}

[Swift] 11 自製ヘルスケアapp / 列挙体による画面遷移

NavigationLinkを使わずに列挙体で画面遷移するようにコーディングしていますが、遷移した際に元の画面が消えずに残ってしまいます。

ところで、XCodeのインデントに縦線が入ると見やすいのですが拡張機能でもできないようです。やはりVSCodeの方が圧倒的に使いやすいです。エディタですから当然ですが。

遷移前
不完全遷移
struct ContentView: View {
    enum ShowView {
        case Home
        case HR_Graph
        case HR_List
        case HRV_Graph
        case HRV_List
    }

    @State var displayMode = ShowView.Home
    @State private var selectedDate_from = Date()
    @State private var selectedDate_to = Date()
    @State private var selection = 1
         
    var body: some View {
        if displayMode == ShowView.HR_Graph {
            ContentView_HR_Graph()
        } else if displayMode == ShowView.HR_List {
            ContentView_HR_List()
        } else if displayMode == ShowView.HRV_Graph {
            ContentView_HRV_Graph()
        } else if displayMode == ShowView.HRV_List {
            ContentView_HRV_List()
        }