I have a TextEditor that calls actionSheet when user tap on it, and displays action sheet content inside. Is it possible to keep TextEditor interactable but not editable ? I've tried .disabled() - but in this case, TextEditor becomes untapable, so user can't call ActionSheet. I would like to avoid using UIViewRepresentable to solving this.
import SwiftUI
import NavigationStack
struct CompleteSessionView: View {
#StateObject var viewModel = CompleteSessionViewModel()
#EnvironmentObject private var navigationStack: NavigationStack
#Binding var isPresented: Bool
#State var sessionManager: SessionManager
#State var showingSkatingStyleSheet = false
var body: some View {
VStack(alignment: .leading, spacing: 5) {
TextBox(text: $viewModel.style, placeholder: "", multiLine:false)
.onTapGesture {
showingSkatingStyleSheet = true
}
}
.actionSheet(isPresented: $showingSkatingStyleSheet) {
styleActionSheet()
}
.onAppear {
viewModel.style = sessionManager.skatingStyle?.localizedString ?? ""
}
}
func styleActionSheet() -> ActionSheet {
return ActionSheet(title: Text("new_sessions_page_style_action_sheet_title".localized), message: Text("new_sessions_page_style_action_sheet_msg".localized), buttons: [
.default(Text(SkatingStyle.streetSkating.localizedString)) {
sessionManager.skatingStyle = .streetSkating
viewModel.style = SkatingStyle.streetSkating.localizedString
},
.default(Text(SkatingStyle.trailSkating.localizedString)) {
sessionManager.skatingStyle = .trailSkating
viewModel.style = SkatingStyle.trailSkating.localizedString
},
.default(Text(SkatingStyle.rollersDance.localizedString)) {
sessionManager.skatingStyle = .rollersDance
viewModel.style = SkatingStyle.rollersDance.localizedString
},
.default(Text(SkatingStyle.freestyle.localizedString)) {
sessionManager.skatingStyle = .freestyle
viewModel.style = SkatingStyle.freestyle.localizedString
},
.default(Text(SkatingStyle.rollerDerby.localizedString)) {
sessionManager.skatingStyle = .rollerDerby
viewModel.style = SkatingStyle.rollerDerby.localizedString
},
.default(Text(SkatingStyle.aggresive.localizedString)) {
sessionManager.skatingStyle = .aggresive
viewModel.style = SkatingStyle.aggresive.localizedString
},
.default(Text(SkatingStyle.indoorRinks.localizedString)) {
sessionManager.skatingStyle = .indoorRinks
viewModel.style = SkatingStyle.indoorRinks.localizedString
},
.default(Text(SkatingStyle.artistic.localizedString)) {
sessionManager.skatingStyle = .artistic
viewModel.style = SkatingStyle.artistic.localizedString
},
.cancel()
])
}
}
struct TextBox: View {
#Binding var text: String
var placeholder: String
var multiLine: Bool
var body: some View {
ZStack(alignment: .topLeading) {
TextEditor(text: $text)
.introspectTextField { textfield in
textfield.returnKeyType = .next
}
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(Color.black, lineWidth: 1)
)
.padding(.horizontal, 8)
if text.isEmpty {
Text(placeholder)
.foregroundColor(.black.opacity(0.25))
.padding(.top, 8)
.padding(.horizontal, 12)
.allowsHitTesting(false)
}
}
}
}
Is it possible to keep TextEditor interactable but not editable?
Yes. You can give a constant to the expected parameter text. You can interact with it the normal way but there is no way to edit the text.
E.g.
struct ContentView: View {
#State var text = "Placeholder"
var body: some View {
SecondView(text: self.$text)
}
}
struct SecondView: View {
#Binding var text: String
var body: some View {
TextEditor(text: .constant(self.text))
.padding(50)
}
}
Related
I want a text field to be selected when it appears on the screen, but I can't get it working. It only works in this example when I tap the button.
struct ContentView: View {
#State var text = "Hello World"
#FocusState var focused: Bool?
var body: some View {
VStack {
TextField("Placeholder", text: $text)
.focused($focused, equals: true)
.onAppear {
focused = true
}
Button {
focused = true
} label: {
Text("Select")
}
}
}
}
Execute focused = true after delay, using asyncAfter().
Try like this:
struct ContentView: View {
#State var text = "Hello World"
#FocusState var focused: Bool?
var body: some View {
VStack {
TextField("Placeholder", text: $text)
.focused($focused, equals: true)
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
self.focused = true
}
}
Button {
focused = true
} label: {
Text("Select")
}
}
}
}
Purpose
I want to update the data in the tabview automatically when I return to the RootView after I rename the tag name in the tagManagemenView.
Current Status
When I do delete operation in the TagManagementView, the RootView is able to update automatically.
However, If the Tag name is modified, the RootView display will not be updated, but clicking into the ItemDetailsView is able to display the latest modified name. Only the display of the RootView is not updated.
Background and code
Item and Tag are created using coredata and have a one-to-many relationship, where one Item corresponds to multiple Tags
// RootView
struct RootView: View {
#State private var selection = 0
var body: some View {
NavigationView {
TabView(selection: $selection) {
ItemListView()
.tabItem {
Label ("Items",systemImage: "shippingbox")
}
.tag(0)
Settings()
.tabItem{
Label("Settings", systemImage: "gearshape")
}
.tag(1)
}
.navigationTitle(selection == 0 ? "Items" : "Settings")
}
.navigationViewStyle(.stack)
}
}
// ItemListView
struct ItemListView: View {
#FetchRequest var items: FetchedResults<Item>
#State private var itemDetailViewIsShow: Bool = false
#State private var selectedItem: Item? = nil
init() {
var predicate = NSPredicate(format: "TRUEPREDICATE"))
_items = FetchRequest(fetchRequest: Item.fetchRequest(predicate))
}
var body: some View {
ForEach(items) { item in
Button(action: {
self.selectedItem = item
self.itemDetailViewIsShow = true
}, label: {
ItemCellView(item: item)
})
}
if selectedItem != nil {
NavigationLink (
destination: ItemDetailView(item: selectedItem!, detailViewIsShow: $itemDetailViewIsShow),
isActive: $itemDetailViewIsShow
) {
EmptyView()
}
.isDetailLink(false)
}
}
}
// TagManagementView
struct TagManagementView: View {
#Environment(\.managedObjectContext) var context
#FetchRequest(entity: Tag.entity(), sortDescriptors: []) var allTags: FetchedResults<Tag>
#State var isShowDeleteAlert = false
#State var showModel = false
#State var selected: Tag?
var body: some View {
ZStack {
List {
ForEach(allTags) { tag in
TagCellView(tag: tag)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(role: .destructive, action: {
isShowDeleteAlert = true
selected = tag
}, label: {
Label("Delete", systemImage: "trash")
.foregroundColor(.white)
})
Button(action: {
showModel = true
selected = tag
}, label: {
Label("Edit", systemImage: "square.and.pencil")
.foregroundColor(.white)
})
}
}
.confirmationDialog("Delete confirm", isPresented: self.$isShowDeleteAlert, titleVisibility: .visible) {
Button("Delete", role: .destructive) {
if self.selected != nil {
self.selected!.delete(context: context)
}
}
Button(role: .cancel, action: {
isShowDeleteAlert = false
}, label: {
Text("Cancel")
.font(.system(size: 17, weight: .medium))
})
}
}
if self.showModel {
// background...
Color("mask").edgesIgnoringSafeArea(.all)
TagEditorModal(selected: self.$selected, isShowing: self.$showModel)
}
}
}
}
// TagEditorModal
struct TagEditorModal: View {
#Environment(\.managedObjectContext) var context
#State var tagName: String = ""
#Binding var isShowing: Bool
#Binding var selector: Tag?
init (selected: Binding<Tag?>, isShowing: Binding<Bool>) {
_isShowing = isShowing
_selector = selected
_tagName = .init(wrappedValue: selected.wrappedValue!.name)
}
var body: some View {
VStack{
TextField("Tag name", text: self.$tagName)
HStack {
Button(action: {
self.isShowing = false
}) {
Text("Cancel")
}
Button(action: {
self.selector!.update(name: self.tagName, context: context)
self.isShowing = false
}, label: {
Text("Submit")
})
}
}
}
}
// update tagName func
extension Tag {
func update(name: String, context: NSManagedObjectContext) {
self.name = name
self.updatedAt = Date()
self.objectWillChange.send()
try? context.save()
}
}
// ItemCellView
struct ItemCellView: View {
#Environment(\.managedObjectContext) var context
#ObservedObject var item: Item
var body: some View {
VStack {
Text(item.name)
TagListView(tags: .constant(item.tags))
}
}
}
// tagListView
struct TagListView: View {
#Binding var tags: [Tag]
#State private var totalHeight = CGFloat.zero
var body: some View {
VStack {
GeometryReader { geo in
VStack(alignment: .leading,spacing: 10) {
ForEach (getRows(screenWidth: geo.size.width), id: \.self) {rows in
HStack(spacing: 4) {
ForEach (rows) { tag in
Text(tag.name)
.font(.system(size: 10))
.fontWeight(.medium)
.lineLimit(1)
.cornerRadius(40)
}
}
}
}
.frame(width: geo.size.width, alignment: .leading)
.background(viewHeightReader($totalHeight))
}
}
.frame(height: totalHeight)
}
private func viewHeightReader(_ binding: Binding<CGFloat>) -> some View {
return GeometryReader { geo -> Color in
let rect = geo.frame(in: .local)
DispatchQueue.main.async {
binding.wrappedValue = rect.size.height
}
return .clear
}
}
func getRows(screenWidth: CGFloat) -> [[Tag]] {
var rows: [[Tag]] = []
var currentRow: [Tag] = []
var totalWidth: CGFloat = 0
self.tags.forEach{ tag in
totalWidth += (tag.size + 24)
if totalWidth > (screenWidth) {
totalWidth = (!currentRow.isEmpty || rows.isEmpty ? (tag.size + 24) : 0)
rows.append(currentRow)
currentRow.removeAll()
currentRow.append(tag)
} else {
currentRow.append(tag)
}
}
if !currentRow.isEmpty {
rows.append(currentRow)
currentRow.removeAll()
}
return rows
}
}
I added a TagCellView and then used an #ObservedObject for the tag
struct TagChipsView: View {
#ObservedObject var tag: Tag
let verticalPadding: CGFloat = 2.0
let horizontalPadding: CGFloat = 8.0
var body: some View {
Text(tag.name)
}
}
I have successfully displayed the language in the UI, but I have a problem: when I click the "Save" button it still doesn't save the language I selected.
I want when I have selected the language and clicked the Save Button , it will return to the previous page and when I click it again it will show the language I selected before.
Above data is simulation only, I mainly focus on its features
All my current code
struct ContentView: View {
var language: String? = ""
var body: some View {
NavigationView {
HStack {
NavigationLink(destination:LanguageView(language: language)) {
Text("Language")
.padding()
Spacer()
Text(language!)
.padding()
}
}
}
}
}
struct LanguageView: View {
#Environment(\.presentationMode) var pres
#State var language: String?
#State var selectedLanguage: String? = ""
var body: some View {
VStack {
CustomLanguageView(selectedLanguage: $language)
Button(action: {
language = selectedLanguage
pres.wrappedValue.dismiss()
})
{
Text("Save")
.foregroundColor(.black)
}
.padding()
Spacer()
}
}
}
struct CustomLanguageView: View {
var language = ["US", "English", "Mexico", "Canada"]
#Binding var selectedLanguage: String?
var body: some View {
LazyVStack {
ForEach(language, id: \.self) { item in
SelectionCell(language: item, selectedLanguage: self.$selectedLanguage)
.padding(.trailing,40)
Rectangle().fill(Color.gray)
.frame( height: 1,alignment: .bottom)
}
.frame(height:15)
}
}
}
struct SelectionCell: View {
let language: String
#Binding var selectedLanguage: String?
var body: some View {
HStack {
Text(language)
Spacer()
if language == selectedLanguage {
Image(systemName: "checkmark")
.resizable()
.frame(width:20, height: 15)
}
}
.onTapGesture {
self.selectedLanguage = self.language
}
}
}
Edit to my previous answer, since something is blocking my edit to my previous answer, this one shows all the code I used to make it works well for me:
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#State var language: String? = ""
var body: some View {
NavigationView {
HStack {
NavigationLink(destination: LanguageView(language: $language)) {
Text("Language").padding()
Spacer()
Text(language!).padding()
}
}
}
}
}
struct LanguageView: View {
#Environment(\.presentationMode) var pres
#Binding var language: String?
var body: some View {
VStack {
CustomLanguageView(selectedLanguage: $language) // <--- here
Button(action: { pres.wrappedValue.dismiss() }) { // <--- here
Text("Save").foregroundColor(.black)
}.padding()
Spacer()
}
}
}
struct CustomLanguageView: View {
var language = ["US", "English", "Mexico", "Canada"]
#Binding var selectedLanguage: String?
var body: some View {
LazyVStack {
ForEach(language, id: \.self) { item in
SelectionCell(language: item, selectedLanguage: self.$selectedLanguage).padding(.trailing,40)
Rectangle().fill(Color.gray).frame( height: 1,alignment: .bottom)
}.frame(height:15)
}
}
}
struct SelectionCell: View {
let language: String
#Binding var selectedLanguage: String?
var body: some View {
HStack {
Text(language)
Spacer()
if language == selectedLanguage {
Image(systemName: "checkmark").resizable().frame(width:20, height: 15)
}
}
.onTapGesture {
self.selectedLanguage = self.language
}
}
}
you could use #State var language throughout and use dismiss, such as this,
to achieve what you want:
struct LanguageView: View {
#Environment(\.dismiss) var dismiss // <--- here
#Binding var language: String?
var body: some View {
VStack {
CustomLanguageView(selectedLanguage: $language) // <--- here
Button(action: {
dismiss() // <--- here
})
{
Text("Save").foregroundColor(.black)
}
.padding()
Spacer()
}
}
}
I want to animate the appearance of an item in a list. The list looks like this:
Text("Jim")
Text("Jonas")
TextField("New Player")
TextField("New Player") //should be animated when appearing (not shown until a name is typed in the first "New Player")
The last TextField should be hidden until newPersonName.count > 0 and then appear with an animation.
This is the code:
struct V_NewSession: View {
#State var newPersonName: String = ""
#State var participants: [String] = [
"Jim",
"Jonas"
]
var body: some View {
VStack(alignment: .leading) {
ForEach(0...self.participants.count + 1, id: \.self) { i in
// Without this if statement, the animation works
// but the Textfield shouldn't be shown until a name is typed
if (!(self.newPersonName.count == 0 && i == self.participants.count + 1)) {
HStack {
if(i < self.participants.count) {
Text(self.participants[i])
} else {
TextField("New Player",
text: $newPersonName,
onEditingChanged: { focused in
if (self.newPersonName.count == 0) {
if (!focused) {
handleNewPlayerEntry()
}
}
})
}
}
}
}
.transition(.scale)
Spacer()
}
}
func handleNewPlayerEntry() {
if(newPersonName.count > 0) {
withAnimation(.spring()) {
participants.append(newPersonName)
newPersonName = ""
}
}
}
}
I know withAnimation(...) only applies to participants.append(newPersonName), but how can I get the animation to work on the property change in the if-statement?
if ((!(self.newPersonName.count == 0 && i == self.participants.count + 1)).animation()) doesn't work.
Your example code won't compile for me, but here's a trivial example of using Combine inside a ViewModel to control whether a second TextField appears based on a condition:
import SwiftUI
import Combine
class ViewModel : ObservableObject {
#Published var textFieldValue1 = ""
#Published var textFieldValue2 = "Second field"
#Published var showTextField2 = false
private var cancellable : AnyCancellable?
init() {
cancellable = $textFieldValue1.sink { value in
withAnimation {
self.showTextField2 = !value.isEmpty
}
}
}
}
struct ContentView: View {
#StateObject var viewModel = ViewModel()
var body: some View {
VStack {
TextField("", text: $viewModel.textFieldValue1)
.textFieldStyle(RoundedBorderTextFieldStyle())
if viewModel.showTextField2 {
TextField("", text: $viewModel.textFieldValue2)
.textFieldStyle(RoundedBorderTextFieldStyle())
.transition(.scale)
}
}
}
}
Is that approaching what you're attempting to build ?
struct ContentView: View {
#State var newPersonName: String = ""
#State var participants: [String] = [
"Jim",
"Jonas"
]
#State var editingNewPlayer = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
ForEach(participants, id: \.self) { participant in
Text(participant)
.padding(.trailing)
Divider()
}
Button(action: handleNewPlayerEntry, label: {
TextField("New Player", text: $newPersonName, onEditingChanged: { edit in
editingNewPlayer = edit
}, onCommit: handleNewPlayerEntry)
})
if editingNewPlayer {
Button(action: handleNewPlayerEntry, label: {
TextField("New Player", text: $newPersonName) { edit in
editingNewPlayer = false
}
})
}
}
.padding(.leading)
.frame(maxHeight: .infinity, alignment: .top)
.transition(.opacity)
.animation(.easeIn)
}
func handleNewPlayerEntry() {
if newPersonName.count > 0 {
withAnimation(.spring()) {
participants.append(newPersonName)
newPersonName = ""
editingNewPlayer = false
}
}
}
}
I have two pages in my app TodayPage and CalendarList page.
I use EnvironmentObject wrapper to pass data between these two pages.
When TodayPage appears on onAppear modifier I call a function to generate days of calendar for me till now everything works fine when I add text to the list of TodayPage then go to the calendarList page and come back again to TodayPage all of the text that I addd to list are gone.I find out I can avoid lost of data by adding simple if to onAppear but I'm not sure this solution is right.
I have to upload lots of code ,Thanks for your help
( DataModel ) :
import SwiftUI
import Foundation
import Combine
struct Day : Identifiable {
var id = UUID()
var name : String
var date : String
var month : String
var List : [Text1?]
}
struct Text1 : Identifiable , Hashable{
var id = UUID()
var name: String
var color: Color
}
class AppState : ObservableObject {
#Published var dataLoaded = false
#Published var allDays : [Day] = [.init(name : "",date: "",month: "",List : [])]
func getDays(number: Int) -> [Day] {
let today = Date()
let formatter = DateFormatter()
return (0..<number).map { index -> Day in
let date = Calendar.current.date(byAdding: .day, value: index, to: today) ?? Date()
return Day(name: date.dayOfWeek(withFormatter: formatter) ?? "", date: "\(Calendar.current.component(.day, from: date))", month: date.nameOfMonth(withFormatter: formatter) ?? "", List: [])
}
}
}
extension Date {
func dayOfWeek(withFormatter dateFormatter: DateFormatter) -> String? {
dateFormatter.dateFormat = "EEEE"
return dateFormatter.string(from: self).capitalized
}
func nameOfMonth(withFormatter dateFormatter: DateFormatter) -> String? {
dateFormatter.dateFormat = "LLLL"
return dateFormatter.string(from: self).capitalized
}
}
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(name: "", color: .clear)] //start with one empty item
func saveToAppState(appState: AppState) {
appState.allDays[0].List.append(contentsOf: textItemsToAdd.filter {
!$0.name.isEmpty })
}
func bindingForId(id: UUID) -> Binding<String> {
.init { () -> String in
self.textItemsToAdd.first(where: { $0.id == id })?.name ?? ""
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return .init(id: id, name: newValue, color: .clear)
}
}
}
}
List view :
struct ListView: View {
#State private var showAddListView = false
#EnvironmentObject var appState : AppState
#Binding var dayList : [Text1?]
var title : String
var body: some View {
NavigationView {
VStack {
ZStack {
List(dayList, id : \.self){ text in
Text(text?.name ?? "")
}
if showAddListView {
AddListView(showAddListView: $showAddListView)
.offset(y:-100)
}
}
}
.navigationTitle(title)
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
pop up menu View(for adding text into the list)
struct AddListView: View {
#Binding var showAddListView : Bool
#EnvironmentObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(name: "", color: .clear)) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in //note this is id: \.id and not \.self
PreAddTextField(textInTextField: viewModel.bindingForId(id: item.id))
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct PreAddTextField: View {
#Binding var textInTextField : String
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var save : () -> Void
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
save()
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
var addItem : () -> Void
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
addItem()
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
TodayPage View :
struct TodayPage: View {
#EnvironmentObject var appState : AppState
var body: some View {
ListView(dayList: $appState.allDays[0].List, title: "Today")
.onAppear {
// To avoid data lost , we can use simple if below but I'm not sure it's a right solution
// if appState.dataLoaded == false {
appState.allDays = appState.getDays(number: 365)
// appState.dataLoaded = true
// }
}
}
}
CalendarListPage :
struct CalendarList: View {
#EnvironmentObject var appState : AppState
var body: some View {
NavigationView {
List {
ForEach(appState.allDays.indices, id:\.self) { index in
NavigationLink(destination: ListView(appState: _appState, dayList: $appState.allDays[index].List, title: appState.allDays[index].name).navigationBarTitleDisplayMode(.inline) ) {
HStack(alignment: .top) {
RoundedRectangle(cornerRadius: 23)
.frame(width: 74, height: 74)
.foregroundColor(Color.blue)
.overlay(
VStack {
Text(appState.allDays[index].date)
.font(.system(size: 35, weight: .regular))
.foregroundColor(.white)
Text(appState.allDays[index].month)
.foregroundColor(.white)
}
)
.padding(.trailing ,4)
VStack(alignment: .leading, spacing: 5) {
Text(appState.allDays[index].name)
.font(.system(size: 20, weight: .semibold))
}
}
.padding(.vertical ,6)
}
}
}
.navigationTitle("Calendar")
}.onAppear {
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
and finally TabBar :
struct TabBar: View {
var body: some View {
let appState = AppState()
TabView {
TodayPage().tabItem {
Image(systemName: "info.circle")
Text("Today")
}
CalendarList().tabItem {
Image(systemName: "square.fill.text.grid.1x2")
Text("Calendar")
}
}
.environmentObject(appState)
}
}
Right now, because your let appState is inside the body of TabBar, it gets recreated every time TabBar is rendered. Instead, store it as a #StateObject (or #ObservedObject if you are pre iOS 14):
struct TabBar: View {
#StateObject var appState = AppState()
var body: some View {
TabView {
TodayPage().tabItem {
Image(systemName: "info.circle")
Text("Today")
}
CalendarList().tabItem {
Image(systemName: "square.fill.text.grid.1x2")
Text("Calendar")
}
}
.onAppear {
appState.allDays = appState.getDays(number: 365)
}
.environmentObject(appState)
}
}
Then, remove your other onAppear on TodayPage