
iPhone アプリを自分でつくる 41.
今回の内容: Tab を使用してアプリを作成する 8.
前回の ToolBox アプリの続きです。
前回から MemoView タブを整えています。
現状のコード
前回第40回最後のコードが現在の MemoViewファイル です。
それ以外のファイルは第39回の最後のコードとなります。
MemoView デザイン 続き
EditSheetView を修正します。
ページのトップには 「編集」と置いて編集中とわかるようにします。
配置は MemoDetailView と大体同じにしておきます。
タイトル、コンテンツなどのプロンプトや TextEditor の枠組みなどは AddMemoView とだいたい同じようにしていきます。
allowsHitTesting(false) も同様に設置します。
// EditSheetView
VStack {
Text("編集")
.font(.title2.weight(.semibold))
.padding(.vertical, 12)
TextField("タイトル", text: $title)
.frame(maxWidth: 530, alignment: .leading)
.font(.title2.weight(.semibold))
.textFieldStyle(.roundedBorder)
.focused($titleIsFocused)
ZStack {
TextEditor(text: $content)
.frame(maxWidth: 530, alignment: .topLeading)
.focused($contentIsFocused)
.overlay(alignment: .topLeading) {
if content.isEmpty {
Text("コンテンツ")
.allowsHitTesting(false)
.foregroundStyle(.secondary.opacity(0.5))
.padding(.top, 8)
.padding(.leading, 6)
}
}
RoundedRectangle(cornerRadius: 6)
.stroke(lineWidth: 0.5)
.foregroundStyle(.gray.opacity(0.5))
.frame(maxWidth: 530)
}
}
.padding(.horizontal, 24)
.padding(.bottom, 30)
登録ボタンの部分はキャンセルボタンも加えてAddView のデザインと同じにします。
// EditSheetView
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
HStack {
Button("キャンセル", action: { dismiss() })
.frame(width: 100, height: 40)
.background(Color.orange.opacity(0.75))
.clipShape(Capsule())
.padding(.leading,40)
Spacer()
Button("登録", action: { editMemo() })
.frame(width: 100, height: 40)
.background(Color.blue.opacity(0.75))
.clipShape(Capsule())
.padding(.trailing,40)
}
.foregroundStyle(.white)
.frame(maxWidth: 400)
.padding(.bottom, 30)
}
}
AddView のときのように背景に色をつけるので、extension に追加します。
色はライトモード用の thinBlue と ダークモード用の aiTetsu をつくります。
extension Color {
static var ivory = Color(red: 248 / 255, green: 245 / 255, blue: 227 / 255)
static var charcoal = Color(red: 49 / 255, green: 49 / 255, blue: 49 / 255)
static var thinBlue = Color(red: 223 / 255, green: 233 / 255, blue: 243 / 255)
static var aiTetsu = Color(red: 36 / 255, green: 50 / 255, blue: 80 / 255)
}
AddView と同じように @Environment(\.colorScheme) によってモードに合わせた表示色にします。
.presentationBackground(colorScheme == .light ? Color.thinBlue : Color.aiTetsu) とします。
// EditSheetView
struct EditSheetView: View {
@EnvironmentObject var memoVM: MemoViewModel
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
// ~ ~ ~
}
.padding(.horizontal, 30)
.padding(.bottom, 30)
.presentationBackground(colorScheme == .light ? Color.thinBlue : Color.aiTetsu)
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
デザインが整いました。
TextField の枠の角に少しズレがあるので、化粧隠しとして少しコードを調整しました。
// EditSheetView
VStack {
Text("編集")
.font(.title2.weight(.semibold))
.padding(.vertical, 12)
TextField("タイトル", text: $title)
.padding(.horizontal, 2)
.frame(maxWidth: 530, alignment: .leading)
.font(.title2.weight(.semibold))
.textFieldStyle(.roundedBorder)
.focused($titleIsFocused)
ZStack {
TextEditor(text: $content)
.frame(maxWidth: 530, alignment: .topLeading)
.focused($contentIsFocused)
.overlay(alignment: .topLeading) {
if content.isEmpty {
Text("コンテンツ")
.allowsHitTesting(false)
.foregroundStyle(.secondary.opacity(0.5))
.padding(.top, 8)
.padding(.leading, 6)
}
}
RoundedRectangle(cornerRadius: 8)
.stroke(lineWidth: 4.0)
.foregroundStyle(colorScheme == .light ? Color.thinBlue : Color.aiTetsu)
.frame(maxWidth: 532)
RoundedRectangle(cornerRadius: 6)
.stroke(lineWidth: 0.5)
.foregroundStyle(.gray.opacity(0.5))
.frame(maxWidth: 530)
.padding(.horizontal, 2)
.padding(.vertical, 2)
}
}
.padding(.horizontal, 24)
.padding(.bottom, 30)
.presentationBackground(colorScheme == .light ? Color.thinBlue : Color.aiTetsu)
同じように AddMemoView も少し調整しました。
// AddMemoView
VStack {
Text("新規メモ")
.font(.title2.weight(.semibold))
.padding(.vertical, 12)
TextField("タイトル", text: $memoTitle)
.padding(.horizontal, 2)
.textFieldStyle(.roundedBorder)
.focused($titleIsFocused)
.padding(.bottom, 8)
ZStack {
TextEditor(text: $memoContent)
.focused($contentIsFocused)
.overlay(alignment: .topLeading) {
if memoContent.isEmpty {
Text("コンテンツ")
.allowsHitTesting(false)
.foregroundStyle(.secondary.opacity(0.5))
.padding(.top, 8)
.padding(.leading, 6)
}
}
RoundedRectangle(cornerRadius: 8)
.stroke(lineWidth: 4.0)
.foregroundStyle(colorScheme == .light ? Color.ivory : Color.charcoal)
.frame(maxWidth: 602)
RoundedRectangle(cornerRadius: 6)
.strokeBorder(lineWidth: 0.5)
.foregroundStyle(.gray.opacity(0.5))
.frame(maxWidth: 600)
.padding(.horizontal, 2)
.padding(.vertical, 2)
}
}
.frame(maxWidth: 600)
.padding(32)
.presentationBackground(colorScheme == .light ? Color.ivory : Color.charcoal)
fullScreenCover
EditSheetView を sheet シートで表示させていますが、これを fullScreenCover で表示させます。
.sheet(item: $editMemo, ~ のところを
.fullScreenCover(item: $editMemo, ~ に変えるだけです。
}
.fullScreenCover(item: $editMemo, onDismiss: {
// .sheet(item: $editMemo, onDismiss: {
editingMode = false
dismiss()
}) { memo in
EditSheetView を fullScreenCover にしましたが、現れるときには sheet と同じく下から上がってくる表示方法です。
ただ問題が発生しました。
キーボードにつけていた "Done" ボタンが現れなくなってしまいました。
下からスワイプしてホーム画面に戻ってからもう一度アプリを立ち上げると"Done" ボタンがついたキーボードの画面が現れるなど少し?です。
また、もとの .sheet() にもどしてみると表示されるのも確認できます。
また .sheet() を使用してメモを追加する AddMemoView でも問題なく表示されます。
なお、iPad ではキーボードにデフォルトでキーボードを下げるアイコンが設置されているので問題にはなりません。
いろいろと試してみましたが、これは SwiftUI のバグだと思われます。
対応方法は2通りしかありません。
あきらめるか、解決策を探し出すか!です
私にも今まで幾度となくバグらしき挙動を切り抜けてきたという自負があります。
手始めにグーグルで調べてみました。
今回はフルスクリーンカバーにした際に表示されなくなりましたが、報告の中にはシートでダメだったけどフルスクリーンカバーにしたら表示された、や iOS を違うバージョンにしたらなおった、など、さまざまな状況を見ることができます。
そしてこの件に関しては多くのバグレポートがApple に報告されているだろうことも見てとることができます。
あきらめましょう。
違う形でキーボードを下げられるようにします。
まず EditSheetView の ToolbarItem の keyboard を削除します。
// EditSheetView
// .toolbar {
// ToolbarItem(placement: .keyboard) {
// HStack {
// Spacer()
// Button("Done") {
// titleIsFocused = false
// contentIsFocused = false
// }
// }
// }
// }
そして TextEditor を囲んでいる RoundedRectangle を ZStack で囲んで、右下にキーボードを下げるボタンを配置します。
// EditSheetView
ZStack(alignment: .bottomTrailing) {
RoundedRectangle(cornerRadius: 8)
.stroke(lineWidth: 4.0)
.foregroundStyle(colorScheme == .light ? Color.thinBlue : Color.aiTetsu)
.frame(maxWidth: 532)
RoundedRectangle(cornerRadius: 6)
.stroke(lineWidth: 0.5)
.foregroundStyle(.gray.opacity(0.5))
.frame(maxWidth: 530)
.padding(.horizontal, 2)
.padding(.vertical, 2)
Button {
titleIsFocused = false
contentIsFocused = false
} label: {
Image(systemName: "keyboard.chevron.compact.down")
.padding()
}
}
次はこのシートの現れ方に少し表情をつけます。
シートを出したときに文字が浮かび上がるような感じにします。
まず、EditSheetView のプロパティに opacityAmount = 0.0 をセットします。
// EditSheetView
@State var opacityAmount: Double = 0.0
var body: some View {
NavigationStack {
VStack {
Text("編集")
opacity
onAppear
withAnimation
EditSheetView のopacityを opacityAmount = 0 に設定しておき onAppear で withAnimation アニメーションを使用して 1.0 に変更することにより、表示直後は透明に近く、シートがセットされた頃に文字が浮かび上がってきます。
// EditSheetView
}
.padding(.horizontal, 24)
.padding(.bottom, 30)
.presentationBackground(colorScheme == .light ? Color.thinBlue : Color.aiTetsu)
.opacity(opacityAmount)
.onAppear {
withAnimation(.linear(duration: 0.5).delay(0.5)) {
opacityAmount = 1.0
}
}
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
今回はここまでです。
次回もメモのデザイン調整を続けます。
まとめ
いかがでしたでしょうか?
うまくいかないことも多々ありますが、解決策を探していて違った方向で思いがけない発見があったりするのも楽しいです。
次回第42回内容 次回は Tab を使用してアプリを作成する 9 です。
よろしくお願いします。
今回までのコード
今回修正した MemoViewファイルを載せています。
これ以外は第39回の最後のコードとなります。
import SwiftUI
struct MemoView: View {
@StateObject var memoVM = MemoViewModel()
@State private var showingAddSheet = false
var body: some View {
NavigationStack {
List($memoVM.memos, editActions: .all) { $memo in
NavigationLink(value: memo) {
Text(memo.memoTitle)
}
}
.navigationDestination(for: Memo.self) { memo in
MemoDetailView(memo: memo)
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
showingAddSheet = true
} label: {
Image(systemName: "plus")
.font(.callout.weight(.bold))
.foregroundStyle(.white)
.padding(2)
.background(.blue.opacity(0.75))
.clipShape(Circle())
}
}
}
.sheet(isPresented: $showingAddSheet) {
AddMemoView()
}
.navigationTitle("Notes")
}
.environmentObject(memoVM)
}
}
struct AddMemoView: View {
@EnvironmentObject var memoVM: MemoViewModel
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
@State private var memoTitle = ""
@State private var memoContent = ""
@FocusState private var titleIsFocused: Bool
@FocusState private var contentIsFocused: Bool
var body: some View {
NavigationStack {
VStack {
Text("新規メモ")
.font(.title2.weight(.semibold))
.padding(.vertical, 12)
TextField("タイトル", text: $memoTitle)
.padding(.horizontal, 2)
.textFieldStyle(.roundedBorder)
.focused($titleIsFocused)
.padding(.bottom, 8)
ZStack {
TextEditor(text: $memoContent)
.focused($contentIsFocused)
.overlay(alignment: .topLeading) {
if memoContent.isEmpty {
Text("コンテンツ")
.allowsHitTesting(false)
.foregroundStyle(.secondary.opacity(0.5))
.padding(.top, 8)
.padding(.leading, 6)
}
}
RoundedRectangle(cornerRadius: 8)
.stroke(lineWidth: 4.0)
.foregroundStyle(colorScheme == .light ? Color.ivory : Color.charcoal)
.frame(maxWidth: 602)
RoundedRectangle(cornerRadius: 6)
.strokeBorder(lineWidth: 0.5)
.foregroundStyle(.gray.opacity(0.5))
.frame(maxWidth: 600)
.padding(.horizontal, 2)
.padding(.vertical, 2)
}
}
.frame(maxWidth: 600)
.padding(32)
.presentationBackground(colorScheme == .light ? Color.ivory : Color.charcoal)
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
HStack {
Button("キャンセル", action: { dismiss() })
.frame(width: 100, height: 40)
.background(Color.orange.opacity(0.75))
.clipShape(Capsule())
.padding(.leading,40)
Spacer()
Button("登録") {
if !memoTitle.isEmpty && !memoContent.isEmpty {
let memo = Memo(memoTitle: memoTitle, memoContent: memoContent)
memoVM.addMemo(memo: memo)
}
dismiss()
}
.frame(width: 100, height: 40)
.background(Color.blue.opacity(0.75))
.clipShape(Capsule())
.padding(.trailing,40)
}
.foregroundStyle(.white)
.frame(maxWidth: 400)
.padding(.bottom, 30)
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
HStack {
Spacer()
Button("Done") {
titleIsFocused = false
contentIsFocused = false
}
}
}
}
}
}
}
struct MemoDetailView: View {
@State private var editingMode = false
@State private var editMemo: Memo? = nil
@Environment(\.dismiss) var dismiss
var memo: Memo
var body: some View {
VStack {
Text(memo.memoTitle)
.frame(maxWidth: 520, alignment: .leading)
.font(.title2.weight(.semibold))
.padding(.top, 20)
.padding(.bottom, 12)
Text(memo.memoContent)
.frame(maxWidth: 520, alignment: .topLeading)
.padding(.bottom, 30)
Spacer()
Toggle("編集モード", isOn: $editingMode)
.padding(.horizontal, 30)
.padding(.bottom, 30)
.frame(maxWidth: 240)
.foregroundStyle(.secondary)
}
.padding(.horizontal, 30)
.onChange(of: editingMode) {
if editingMode {
editMemo = memo
}
}
.fullScreenCover(item: $editMemo, onDismiss: {
editingMode = false
dismiss()
}) { memo in
EditSheetView(memo: memo, title: memo.memoTitle, content: memo.memoContent)
}
}
}
struct EditSheetView: View {
@EnvironmentObject var memoVM: MemoViewModel
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
@State var memo: Memo
@State var title: String
@State var content: String
@FocusState private var titleIsFocused: Bool
@FocusState private var contentIsFocused: Bool
@State var opacityAmount: Double = 0.0
var body: some View {
NavigationStack {
VStack {
Text("編集")
.font(.title2.weight(.semibold))
.padding(.vertical, 12)
TextField("タイトル", text: $title)
.padding(.horizontal, 2)
.frame(maxWidth: 530, alignment: .leading)
.font(.title2.weight(.semibold))
.textFieldStyle(.roundedBorder)
.focused($titleIsFocused)
ZStack {
TextEditor(text: $content)
.frame(maxWidth: 530, alignment: .topLeading)
.focused($contentIsFocused)
.overlay(alignment: .topLeading) {
if content.isEmpty {
Text("コンテンツ")
.allowsHitTesting(false)
.foregroundStyle(.secondary.opacity(0.5))
.padding(.top, 8)
.padding(.leading, 6)
}
}
ZStack(alignment: .bottomTrailing) {
RoundedRectangle(cornerRadius: 8)
.stroke(lineWidth: 4.0)
.foregroundStyle(colorScheme == .light ? Color.thinBlue : Color.aiTetsu)
.frame(maxWidth: 532)
RoundedRectangle(cornerRadius: 6)
.stroke(lineWidth: 0.5)
.foregroundStyle(.gray.opacity(0.5))
.frame(maxWidth: 530)
.padding(.horizontal, 2)
.padding(.vertical, 2)
Button {
titleIsFocused = false
contentIsFocused = false
} label: {
Image(systemName: "keyboard.chevron.compact.down")
.padding()
}
}
}
}
.padding(.horizontal, 24)
.padding(.bottom, 30)
.presentationBackground(colorScheme == .light ? Color.thinBlue : Color.aiTetsu)
.opacity(opacityAmount)
.onAppear {
withAnimation(.linear(duration: 0.5).delay(0.5)) {
opacityAmount = 1.0
}
}
.toolbar {
ToolbarItemGroup(placement: .bottomBar) {
HStack {
Button("キャンセル", action: { dismiss() })
.frame(width: 100, height: 40)
.background(Color.orange.opacity(0.75))
.clipShape(Capsule())
.padding(.leading,40)
Spacer()
Button("登録", action: { editMemo() })
.frame(width: 100, height: 40)
.background(Color.blue.opacity(0.75))
.clipShape(Capsule())
.padding(.trailing,40)
}
.foregroundStyle(.white)
.frame(maxWidth: 400)
.padding(.bottom, 30)
}
}
}
}
func editMemo() {
let newTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
let newContent = content.trimmingCharacters(in: .whitespacesAndNewlines)
if !newTitle.isEmpty && !newContent.isEmpty {
memoVM.editMemo(id: memo.id, title: newTitle, content: newContent)
}
dismiss()
}
}
struct Memo: Identifiable, Hashable, Codable {
var id = UUID()
var memoTitle: String
var memoContent: String
static let sampleMemos: [Memo] = [
Memo(memoTitle: "Title サンプル1", memoContent: "Contents サンプル1"),
Memo(memoTitle: "Title サンプル2", memoContent: "Contents サンプル2"),
Memo(memoTitle: "Title サンプル3", memoContent: "Contents サンプル3"),
Memo(memoTitle: "Title サンプル4", memoContent: "Contents サンプル4"),
Memo(memoTitle: "Title サンプル5", memoContent: "Contents サンプル5")
]
}
class MemoViewModel: ObservableObject {
@Published var memos: [Memo] = []
let savePath = URL.documentsDirectory.appending(path: "SaveMemoData")
init() {
setMemos()
}
func setMemos() {
if let data = try? Data(contentsOf: savePath) {
if let result = try? JSONDecoder().decode([Memo].self, from: data){
memos = result
}
} else {
memos = []
memos.append(contentsOf: Memo.sampleMemos)
}
}
func saveMemos() {
do {
let encodedMemos = try JSONEncoder().encode(memos)
try encodedMemos.write(to: savePath, options: [.atomic, .completeFileProtection])
} catch {
print(error.localizedDescription)
}
}
func addMemo(memo: Memo) {
memos.append(memo)
saveMemos()
}
func editMemo(id: UUID, title: String, content: String) {
if let index = memos.firstIndex(where: { $0.id == id }) {
if memos[index].memoTitle != title || memos[index].memoContent != content {
memos[index].memoTitle = title
memos[index].memoContent = content
saveMemos()
}
}
}
}
extension Color {
static var ivory = Color(red: 248 / 255, green: 245 / 255, blue: 227 / 255)
static var charcoal = Color(red: 49 / 255, green: 49 / 255, blue: 49 / 255)
static var thinBlue = Color(red: 223 / 255, green: 233 / 255, blue: 243 / 255)
static var aiTetsu = Color(red: 36 / 255, green: 50 / 255, blue: 80 / 255)
}
#Preview {
MemoView()
.environmentObject(MemoViewModel())
}
以上です
いいなと思ったら応援しよう!
