I'm writing a macOS app in Swiftui, for Big Sur and newer. It's a three pane navigationview app, where the left most pane has the list of options (All Notes in this case), the middle pane is a list of the actual items (title and date), and the last one is a TextEditor where the user adds text.
Each pane is a view that calls the the next view via a NavigationLink. Here's the basic code for that.
struct NoteItem: Codable, Hashable, Identifiable {
let id: Int
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
struct ContentView: View {
#State var selection: Set<Int> = [0]
var body: some View {
NavigationView {
List(selection: self.$selection) {
NavigationLink(destination: AllNotes()) {
Label("All Notes", systemImage: "doc.plaintext")
}
.tag(0)
}
.listStyle(SidebarListStyle())
.frame(minWidth: 100, idealWidth: 150, maxWidth: 200, maxHeight: .infinity)
Text("Select a note...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
struct AllNotes: View {
#State var items: [NoteItem] = {
guard let data = UserDefaults.standard.data(forKey: "notes") else { return [] }
if let json = try? JSONDecoder().decode([NoteItem].self, from: data) {
return json
}
return []
}()
#State var noteText: String = ""
var body: some View {
NavigationView {
List(items) { item in
NavigationLink(destination: NoteView()) {
VStack(alignment: .leading) {
Text(item.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(item.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
Text("Select a note...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
NewNote()
}) {
Image(systemName: "square.and.pencil")
}
}
}
}
struct NoteView: View {
#State var text: String = ""
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text).padding().font(.body)
.onChange(of: text, perform: { value in
print("Value of text modified to = \(text)")
})
Spacer()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
}
}
When I create a new note, how can I save the text the user added on the TextEditor in NoteView in the array loaded in AllNotes so I could save the new text? Ideally there is a SaveNote() function that would happen on TextEditor .onChange. But again, given that the array lives in AllNotes, how can I update it from other views?
Thanks for the help. Newbie here!
use EnvironmentObject in App
import SwiftUI
#main
struct NotesApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(DataModel())
}
}
}
now DataModel is a class conforming to ObservableObject
import SwiftUI
final class DataModel: ObservableObject {
#AppStorage("notes") public var notes: [NoteItem] = []
}
any data related stuff should be done in DataModel not in View, plus you can access it and update it from anywhere, declare it like this in your ContentView or any child View
NoteView
import SwiftUI
struct NoteView: View {
#EnvironmentObject private var data: DataModel
var note: NoteItem
#State var text: String = ""
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text).padding().font(.body)
.onChange(of: text, perform: { value in
guard let index = data.notes.firstIndex(of: note) else { return }
data.notes[index].text = value
})
Spacer()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
.onAppear() {
print(data.notes.count)
}
}
}
AppStorage is the better way to use UserDefaults but AppStorage does not work with custom Objects yet (I think it does for iOS 15), so you need to add this extension to make it work.
import SwiftUI
struct NoteItem: Codable, Hashable, Identifiable {
let id: UUID
var text: String
var date = Date()
var dateText: String {
let df = DateFormatter()
df.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return df.string(from: date)
}
var tags: [String] = []
}
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
Now I changed AllNotes view to work with new changes
struct AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(destination: NoteView(note: note)) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
Text("Select a note...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
}
}
}
Related
Here's my code, I don't get why this results in this transparent overlay when I move focus to the Text or SecureField components
struct SettingsPane: View {
// MARK: View state
#State private var isSecured: Bool = true
#State private var showApiKeyPopover: Bool = false
// MARK: Preference Storage
#AppStorage("preference_showWorkboard") var showWorkboard = true
#AppStorage("preference_userName") var userName = ""
#AppStorage("preference_jiraApiKey") var jiraApiKey = ""
// MARK: View dimensions
var frameWidth:CGFloat = 200
var body: some View {
Form {
Section(header: Text("Credentials")) {
TextField("User", text: $userName)
.textContentType(.username)
.frame(width: frameWidth)
HStack(alignment: .center) {
Group {
if isSecured {
SecureField("Jira API Key", text: $jiraApiKey)
.textContentType(.password)
.frame(width: frameWidth)
.lineLimit(1)
} else {
TextField("Jira API Key", text: $jiraApiKey)
.textContentType(.password)
.frame(width: frameWidth)
.lineLimit(1)
}
}.popover(isPresented: $showApiKeyPopover) {
Text("To get an API key visit your Jira profile.\nClick 'Personal Access Tokens' and create one named 'Firehose' and add it to this field")
.padding()
}
Button(action: {
isSecured.toggle()
}) {
Image(systemName: self.isSecured ? "eye.slash" : "eye")
.accentColor(.gray)
}
.buttonStyle(.borderless)
Button(action: {
showApiKeyPopover.toggle()
}) {
Image(systemName: "questionmark.circle.fill")
.accentColor(.gray)
}
.buttonStyle(.borderless)
}
Divider()
Section(header: Text("Features")) {
Toggle("Show Workboard", isOn: $showWorkboard)
}
}
}
.padding()
.frame(minWidth: 400, maxWidth: 400)
}
}
It works fine with me (macOS 13.0, Xcode 14.1). I get this ...
Could it be some password utility interfering?
I have a simple notes app that uses a list on the left and a texteditor on the right. The list has the titles of the notes, and the texteditor its text. When the user changes the text on the note, the list gets sorted to display the most current note (by date) on top, as its first item.
Started with Ventura, if I'm working on a note other than the first one, then that note (the item in the list) jumps to the top when the text is changed, however, if the first item in the list not visible (I'm working on a note that is way down), then when I change the text the item, it jumps to the top, but you don't jump with it. You are now in this state where you have to scroll up to get to the top and reselect the first item.
I tried using DispatchQueue.main.async to force to reselect the item onchange, but regardless of what I try, it doesn't scroll to the top, even when the selected note id is the correct one.
I ran out of ideas or things to try. How can I go back to the first item once the text is changed?
Here's the code:
struct NoteItem: Codable, Hashable, Identifiable {
let id: Int
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
final class DataModel: ObservableObject {
#AppStorage("notes") public var notes: [NoteItem] = []
}
struct AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
#State var selectedNoteId: UUID?
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(
destination: NoteView(note: note),
tag: note.id,
selection: $selectedNoteId
) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
}
.onAppear {
DispatchQueue.main.async {
selectedNoteId = data.notes.first?.id
}
}
.onChange(of: data.notes) { notes in
if selectedNoteId == nil || !notes.contains(where: { $0.id == selectedNoteId }) {
selectedNoteId = data.notes.first?.id
}
}
}
}
struct NoteView: View {
#EnvironmentObject private var data: DataModel
var note: NoteItem
#State var text: String = ""
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text).padding().font(.body)
.onChange(of: text, perform: { value in
guard let index = data.notes.firstIndex(of: note) else { return }
data.notes[index].text = value
data.sortList()
})
Spacer()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
I have an image background, which should stay in place when the keyboard shows, but instead it moves up together with everything on the screen. I saw someone recommend using ignoresSafeArea(.keyboard), and this question Simple SwiftUI Background Image keeps moving when keyboard appears, but neither works for me. Here is my super simplified code sample. Please keep in mind that while the background should remain unchanged, the content itself should still avoid the keyboard as usual.
struct ProfileAbout: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("write something", text: $text)
Spacer()
Button("SomeButton") {}
}
.background(
Image("BackgroundName")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.keyboard)
)
}
}
Here a possible salvation:
import SwiftUI
struct ContentView: View {
#Environment(\.verticalSizeClass) var verticalSizeClass
#State var valueOfTextField: String = String()
var body: some View {
GeometryReader { proxy in
Image("Your Image name here").resizable().scaledToFill().ignoresSafeArea()
ZStack {
if verticalSizeClass == UserInterfaceSizeClass.regular { TextFieldSomeView.ignoresSafeArea(.keyboard) }
else { TextFieldSomeView }
VStack {
Spacer()
Button(action: { print("OK!") }, label: { Text("OK").padding(.horizontal, 80.0).padding(.vertical, 5.0).background(Color.yellow).cornerRadius(5.0) }).padding()
}
}
.position(x: proxy.size.width/2, y: proxy.size.height/2)
}
}
var TextFieldSomeView: some View {
return VStack {
Spacer()
TextField("write something", text: $valueOfTextField).padding(5.0).background(Color.yellow).cornerRadius(5.0).padding()
Spacer()
}
}
}
u can use GeometryReader
get parent View size
import SwiftUI
import Combine
struct KeyboardAdaptive: ViewModifier {
#State private var keyboardHeight: CGFloat = 0
func body(content: Content) -> some View {
GeometryReader { geometry in
content
.padding(.bottom, keyboardHeight)
.onReceive(Publishers.keyboardHeight) {
self.keyboardHeight = $0
}
}
}
}
extension Publishers {
static var keyboardHeight: AnyPublisher<CGFloat, Never> {
let willShow = NotificationCenter.default.publisher(for: UIApplication.keyboardWillShowNotification)
.map { $0.keyboardHeight }
let willHide = NotificationCenter.default.publisher(for: UIApplication.keyboardWillHideNotification)
.map { _ in CGFloat(0) }
return MergeMany(willShow, willHide)
.eraseToAnyPublisher()
}
}
extension View {
func keyboardAdaptive() -> some View {
ModifiedContent(content: self, modifier: KeyboardAdaptive())
}
}
struct ProfileAbout: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("write something", text: $text)
Spacer()
Button("SomeButton") {}
}
.background(
Image("BackgroundName")
.resizable()
.aspectRatio(contentMode: .fill)
.ignoresSafeArea(.keyboard)
)
.keyboardAdaptive()
}
}
I want to replace a View with another when a Button is pressed in SwiftUI on MacOS (AppKit not Catalyst).
I tried with a NavigationLink like this
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink( destination: Text("DESTINATIONVIEW").frame(maxWidth: .infinity, maxHeight: .infinity) ,
label: {Text("Next")} )
}.frame(width: 500.0, height: 500.0)
}
}
The Destination View is presented like in iPadOS, like a Master-Detail Presentation right of the Source View.
What I want, is to replace the SourceView with the Destination (almost like in iOS)
Another approach was to switch the presented view with a bool variable
struct MasterList: View
{
private let names = ["Frank", "John", "Tom", "Lisa"]
#State var showDetail: Bool = false
#State var selectedName: String = ""
var body: some View
{
VStack
{
if (showDetail)
{ DetailViewReplace(showDetail:$showDetail, text: "\(selectedName)" )
} else
{
List()
{
ForEach(names, id: \.self)
{ name in
Button(action:
{
self.showDetail.toggle()
self.selectedName = name
})
{ Text("\(name)")}
.buttonStyle( LinkButtonStyle() )
}
}.listStyle( SidebarListStyle())
}
}
}
}
struct DetailViewReplace: View {
#Binding var showDetail: Bool
let text: String
var body: some View
{
VStack
{
Text(text)
.frame(maxWidth: .infinity, maxHeight: .infinity)
Button(action:
{ self.showDetail.toggle()
})
{ Text("Return")
}
}
}
}
But that seems quite cumbersome for me if dealing with many views.
Is there a best practice to do that?
How can I do something like that in SwiftUI?
Reckon this view is built in Cocoa, since I can't even layout List and GroupBox properly: strange border appears.
There is no Table view in SwiftUI, there is no NSSegmentedControl.
This is the code I got so far:
import SwiftUI
struct DetailView: View {
let text: String
var body: some View {
GroupBox {
Text(text)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.padding(.leading, 20)
.frame(width:300, height:300)
}
}
struct ContentView: View {
private let names = ["One", "Two", "Three"]
#State private var selection: String? = "One"
var body: some View {
NavigationView {
List(selection: $selection) {
Section(header:
Text("Header")) {
ForEach(names, id: \.self) { name in
NavigationLink(destination: DetailView(text: name)) {
Text(name)
}
}
}
}.frame(width: 200, height: 300).padding(10).border(Color.green, width: 0)
DetailView(text: self.selection ?? "none selected")
}.padding(10)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
import AppKit
struct ContentView: View {
var body: some View {
HStack(spacing: 20) {
PrinterPicker()
.frame(width: 160)
PrinterDetail()
.frame(width: 320)
} //
.padding()
}
}
struct PrinterPicker: View {
var body: some View {
VStack(spacing: 0) {
PrinterList()
PrinterListToolbar()
} //
.border(Color(NSColor.gridColor), width: 1)
}
}
struct PrinterList: View {
var body: some View {
List {
Text("Printer 1")
.font(Font.system(size: 15))
Text("Printer 2")
.font(Font.system(size: 15))
}
}
}
struct PrinterListToolbar: View {
var body: some View {
HStack(spacing: 0) {
ListButton(imageName: NSImage.addTemplateName)
Divider()
ListButton(imageName: NSImage.removeTemplateName)
Divider()
Spacer()
} //
.frame(height: 20)
}
}
struct ListButton: View {
var imageName: String
var body: some View {
Button(action: {}) {
Image(nsImage: NSImage(named: imageName)!)
.resizable()
} //
.buttonStyle(BorderlessButtonStyle())
.frame(width: 20, height: 20)
}
}
struct PrinterDetail: View {
var body: some View {
HStack {
Spacer()
VStack {
Spacer()
Text("No printers are available.")
Text("Click Add (+) to set up a printer.")
Spacer()
}
Spacer()
} //
.font(Font.system(size: 15))
.background(Color(
NSColor.unemphasizedSelectedContentBackgroundColor))
.cornerRadius(6)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(lineWidth: 1)
.foregroundColor(Color(NSColor.gridColor)))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}