SwiftUI - how to increase Toggle's target touch size? - swift

I want to increase Toggle's target size for usability purposes.
It's easy with buttons using .frame(width: 200, height: 200) . But I don't know how to apply the same for toggles.
Here is my code. I have marked where it works and where it doesn't.
import SwiftUI
struct ContentView: View {
#State private var test = true
var body: some View {
VStack {
Text("Hello, world!")
.padding()
Button(action: {
print("button pressed")
}) {
Image(systemName: "checkmark.circle.fill")
.font(.largeTitle)
// This works for Button
.frame(width: 200, height: 200)
}
Toggle(isOn: Binding<Bool>(
get: { test },
set: {
test = $0
})) {
Text("Done")
}
// This doesn't work for Toggle
.frame(width: 200, height: 200)
.labelsHidden()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

You can try something like this:
Toggle(isOn: Binding<Bool>(
get: { test },
set: {
test = $0
})) {
Text("Done")
}
.frame(width: 200, height: 200)
.labelsHidden()
.contentShape(Rectangle())
.onTapGesture {
withAnimation {
test.toggle()
}
}

Related

Keeping text on TextEditor element while switching views SwiftUI

Well, I'm working on a little app where I have a TextEditor element to type whatever we want. The case is, I want to keep the text on the TextEditor while switching other views, but I can't.
TextEditor before switching the view :
TextEditor after switching the view :
The code is the next one:
struct VistaDatos: View {
#State private var opinion: String = ""
var progreso : Double {
Double(opinion.count)
}
var body: some View {
VStack{
//SOME CODE HERE ...
HStack{
Text("Mi opinión...")
.font(.headline)
Image(systemName: "pencil")
.foregroundColor(.white)
.font(.headline)
}
VStack{
TextEditor(text: $opinion)
.background(.green)
.frame(width: 350, height: 250)
.background().colorMultiply(.green)
.overlay(Rectangle().stroke(Color.black, lineWidth:2))
.disableAutocorrection(true)
.onChange(of: self.opinion) { value in
if Int(opinion.count) > 150 {
self.opinion = String(value.prefix(150))
}
}
Text("Número de palabras: \(Int(progreso))/150").foregroundColor(Int(progreso) >= 100 ? .red : .white)
ProgressView(,value: progreso, total: 150) {
}.frame(width: 350, alignment: .center)
}
}.background(Color.green)
Spacer()
}
}
I have to use .onDisappear event to make it work (it seems to be on the first level stack ), but it isn't working...
How can I make it work?
Thanks in advance.
Since you say you have multiple views, that I assume may need the opinion text,
try this example code. It keeps your text in a ObservableObject,
that you can use throughout your app.
For you to do, is to code the save and retrieve
from wherever you want. In this example it is using the UserDefaults.
class StoreService: ObservableObject {
// your text
#Published var opinion = ""
// ... todo code to store your data when you are finished
func save() {
// save your data
UserDefaults.standard.set(opinion, forKey: "opinion")
}
// ... todo code to retrieve your data when the app starts again
init() {
// get your data
opinion = UserDefaults.standard.string(forKey: "opinion") ?? ""
}
}
struct ContentView: View {
#StateObject var store = StoreService() // <-- here
var body: some View {
NavigationStack {
VStack (spacing: 50) {
Text("\(store.opinion.count) characters typed")
NavigationLink("go to VistaDatos", value: "editor")
.navigationDestination(for: String.self) { str in
VistaDatos()
}
}
}.environmentObject(store) // <-- here
}
}
struct VistaDatos: View {
#EnvironmentObject var store: StoreService // <-- here
var progreso : Double {
Double(store.opinion.count)
}
var body: some View {
VStack{
//SOME CODE HERE ...
HStack{
Text("Mi opinión...").font(.headline)
Image(systemName: "pencil")
.foregroundColor(.white)
.font(.headline)
}
VStack{
TextEditor(text: $store.opinion) // <-- here
.background(.green)
.frame(width: 350, height: 250)
.background().colorMultiply(.green)
.overlay(Rectangle().stroke(Color.black, lineWidth:2))
.disableAutocorrection(true)
.onChange(of: store.opinion) { value in
if store.opinion.count > 150 {
store.opinion = String(value.prefix(150))
}
}
Text("Número de palabras: \(Int(progreso))/150").foregroundColor(Int(progreso) >= 100 ? .red : .white)
ProgressView(value: progreso, total: 150).frame(width: 350, alignment: .center)
Button("Save me") { // <-- here
store.save()
}
}
}.background(Color.green)
Spacer()
}
}
Alternatively, you could use the simple #AppStorage, like this:
struct ContentView: View {
#AppStorage("opinion") var opinion = "" // <-- here
var body: some View {
NavigationStack {
VStack (spacing: 50) {
Text("\(opinion.count) characters typed")
NavigationLink("go to VistaDatos", value: "editor")
.navigationDestination(for: String.self) { str in
VistaDatos()
}
}
}
}
}
struct VistaDatos: View {
#AppStorage("opinion") var opinion = "" // <-- here
var progreso : Double {
Double(opinion.count)
}
var body: some View {
VStack{
//SOME CODE HERE ...
HStack{
Text("Mi opinión...").font(.headline)
Image(systemName: "pencil")
.foregroundColor(.white)
.font(.headline)
}
VStack{
TextEditor(text: $opinion) // <-- here
.background(.green)
.frame(width: 350, height: 250)
.background().colorMultiply(.green)
.overlay(Rectangle().stroke(Color.black, lineWidth:2))
.disableAutocorrection(true)
.onChange(of: opinion) { value in
if opinion.count > 150 {
opinion = String(value.prefix(150))
}
}
Text("Número de palabras: \(Int(progreso))/150").foregroundColor(Int(progreso) >= 100 ? .red : .white)
ProgressView(value: progreso, total: 150).frame(width: 350, alignment: .center)
}
}.background(Color.green)
Spacer()
}
}

SwiftUI: save the state of toggle and keep the animation

In SwiftUI, for this code to toggle the display of view:
#State var show = true
Button { withAnimation { show.toggle() }}
label: { Image(systemName: show ? "chevron.down" : "chevron.right") }
if show { ... }
The animation will be shown if the show is the #State variable.
However, I found that if show is changed to #AppStorage (so to keep the show state), the animation will not be shown.
Is there a way to keep the show state and also preserve the animation?
You can also replace the withAnimation {} with the .animation(<#T##animation: Animation?##Animation?#>, value: <#T##Equatable#>) modifier and then it seems to work directly with the #AppStorage wrapped variable.
import SwiftUI
struct ContentView: View {
#AppStorage("show") var show: Bool = true
var body: some View {
VStack {
Button {
self.show.toggle()
}
label: {
Rectangle()
.fill(Color.red)
.frame(width: self.show ? 200 : 400, height: 200)
.animation(.easeIn, value: self.show)
}
Rectangle()
.fill(Color.red)
.frame(width: self.show ? 200 : 400, height: 200)
.animation(.easeIn, value: self.show)
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
EDIT: Following the comments, another solution
import SwiftUI
struct ContentView: View {
#State private var show: Bool
init() {
self.show = UserDefaults.standard.bool(forKey: "show")
// Or self._show = State(initialValue: UserDefaults.standard.bool(forKey: "show"))
}
var body: some View {
VStack {
Button {
withAnimation {
self.show.toggle()
}
}
label: {
Text("Toggle")
}
if show {
Rectangle()
.fill(Color.red)
.frame(width: 200 , height: 200)
}
}
.padding()
.onChange(of: self.show) { newValue in
UserDefaults.standard.set(newValue, forKey: "show")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Image not appearing when using .transition

struct BEHOLD: View {
#State private var showdeatail = false
var body: some View {
VStack {
Button(action: {
showdeatail.toggle()
}) {
Text("S U M M O N\nH I M")
}
.frame(width: 100, height: 50)
.background(Color.red)
.cornerRadius(10)
if showdeatail {
Image("HIM")
.frame(width: 100, height: 100)
.transition(.scale)
}
}
}
So I'm trying to make this image appear when you hit a button and the code seems to be working except the Image seems to be invisible and I'm not sure why
Try this code, it would work!
struct BEHOLD: View {
#State private var showdeatail = false
var body: some View {
VStack {
Button(action: {
showdeatail.toggle()
}) {
Text("S U M M O N\nH I M")
}
.background(Color.red)
.cornerRadius(10)
if showdeatail {
Image(systemName: "person")
.resizable()
.frame(width: 100, height: 100)
.transition(.scale)
}
}
.animation(.default, value: showdeatail)
}
}

How do I implement #EnvironmentObject for this custom full-screen modal setup?

My goal is to have custom modals present over an root view that is essentially a tabbed view. So, I wrapped the TabView in a ZStack and am using an ObservableOBject. But I don't feel I'm doing it the right way.
In my other file, I have the Custom modal "subviews" which has an enum, too, which I think is the right approach to take. But I cannot figure out how to dismiss a modal after it is visible.
It must be #EnvironmentObject, but I don't know what if anything to put in the scene delegate, etc. ("Hacking with Swift" is failing me here, although it's a great resource.)
My idea is that views from the tabbed view will have various buttons which present different modal views, populated later with data specific to say a user and set of fields for data entry.
Right now, I just want to understand how to present and dismiss them.
Here is my root view
import SwiftUI
struct ContentView: View {
#ObservedObject var modal = CustomModal()
var body: some View {
ZStack {
TabView {
ZStack {
Color.pink.opacity(0.2)
Button(action: {
withAnimation{
self.modal.visibleModal = VisibleModal.circle
}
}) {
Text("Circle").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.pink.opacity(0.5)).foregroundColor(.white)
.cornerRadius(12)
}
.tabItem{
VStack{
Image(systemName: "1.square.fill")
Text("One")
}
}.tag(1)
ZStack {
Color.blue.opacity(0.2)
Button(action: {
self.modal.visibleModal = VisibleModal.squircle
}) {
Text("Square").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.blue.opacity(0.5)).foregroundColor(.white)
.cornerRadius(12)
}
.tabItem{
VStack{
Image(systemName: "2.square.fill")
Text("Two")
}
}.tag(2)
}.accentColor(.purple)
VStack {
containedView()
}
}
}
func containedView() -> AnyView {
switch modal.visibleModal {
case .circle: return AnyView(CircleView())
case .squircle: return AnyView(SquircleView())
case .none: return AnyView(Text(""))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And here is my second file with the enum and "subview" modals
import SwiftUI
class CustomModal: ObservableObject {
#Published var visibleModal: VisibleModal = VisibleModal.none
}
enum VisibleModal {
case circle, squircle, none
}
struct CircleView: View {
var body: some View {
ZStack {
Color.pink.blur(radius: 0.4)
Circle().fill()
.frame(width: 300)
.foregroundColor(Color.white.opacity(0.75))
dismissButton()
}.edgesIgnoringSafeArea(.all)
}
}
struct SquircleView: View {
var body: some View {
ZStack{
Color.green.blur(radius: 0.4)
RoundedRectangle(cornerRadius: 48, style: .continuous)
.frame(width: 300, height: 300).foregroundColor(Color.white.opacity(0.75))
dismissButton()
}.edgesIgnoringSafeArea(.all)
}
}
struct dismissButton: View {
#ObservedObject var modal = CustomModal()
var body: some View {
VStack{
Spacer()
Button(action: {
self.modal.visibleModal = VisibleModal.none
}) {
Text("Dismiss").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.white.opacity(0.35)).foregroundColor(.white)
.cornerRadius(12)
.padding(.bottom, 44)
}
}
}
Are you just trying to pass your observable object to the new view?
func containedView() -> some View {
switch modal.visibleModal {
case .circle: return CircleView()
.environmentObject(self.modal)
case .squircle: return SquircleView()
.environmentObject(self.modal)
case .none: return Text("")
}
}
Unless I am misunderstanding the question.
Okay, after a lot of fiddling, it works.
Now my code is as follows.
Root view
struct ContentView: View {
#EnvironmentObject var isModalVisible: CustomModal
#ObservedObject var modal = CustomModal()
var body: some View {
ZStack {
TabView {
ZStack {
Color.pink.opacity(0.2)
Button(action: {
withAnimation{
self.isModalVisible.isModalVisible.toggle()
self.modal.currentModal = VisibleModal.circle
}
}) {
Text("Circle").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.pink.opacity(0.5)).foregroundColor(.white)
.cornerRadius(12)
}
.tabItem{
VStack{
Image(systemName: "1.square.fill")
Text("One")
}
}.tag(1)
ZStack {
Color.blue.opacity(0.2)
Button(action: {
self.isModalVisible.isModalVisible.toggle()
self.modal.currentModal = VisibleModal.squircle
}) {
Text("Square").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.blue.opacity(0.5)).foregroundColor(.white)
.cornerRadius(12)
}
.tabItem{
VStack{
Image(systemName: "2.square.fill")
Text("Two")
}
}.tag(2)
}.accentColor(.purple)
if self.isModalVisible.isModalVisible {
VStack {
containedView()
}
}
}
}
func containedView() -> AnyView {
switch modal.currentModal {
case .circle: return AnyView(CircleView())
case .squircle: return AnyView(SquircleView())
case .none: return AnyView(Text(""))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(CustomModal())
}
}
and the second file with the supporting views and classes and enums:
import SwiftUI
class CustomModal: ObservableObject {
#Published var isModalVisible = false
#Published var currentModal: VisibleModal = .none
}
enum VisibleModal {
case circle, squircle, none
}
struct CircleView: View {
#EnvironmentObject var env: CustomModal
var body: some View {
ZStack {
Color.pink.blur(radius: 0.4)
Circle().fill()
.frame(width: 300)
.foregroundColor(Color.white.opacity(0.75))
dismissButton()
}.edgesIgnoringSafeArea(.all)
}
}
struct SquircleView: View {
var body: some View {
ZStack{
Color.green.blur(radius: 0.4)
RoundedRectangle(cornerRadius: 48, style: .continuous)
.frame(width: 300, height: 300).foregroundColor(Color.white.opacity(0.75))
dismissButton()
}.edgesIgnoringSafeArea(.all)
}
}
struct dismissButton: View {
#EnvironmentObject var env: CustomModal
var body: some View {
VStack{
Spacer()
Button(action: {
self.env.isModalVisible.toggle()
print("TAPPED")
}) {
Text("Dismiss").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.white.opacity(0.35)).foregroundColor(.white)
.cornerRadius(12)
.padding(.bottom, 44)
}
}
}
It still can be refactored. I'm sure. I'd also be happy to hear any comments on how to improve it. But it seems to work.
NOTE: This code ContentView().environmentObject(CustomModal()) is put in the previewP{rovider code and in SceneDelegate.

Tabbar middle button utility function in SwiftUI

I'm trying to reproduce a "Instagram" like tabBar which has a "Utility" button in the middle which doesn't necessarily belong to the tabBar eco system.
I have attached this gif to show the behaviour I am after. To describe the issue. The tab bar in the middle (Black plus) is click a ActionSheet is presented INSTEAD of switching the view.
How I would do this in UIKit is simply use the
override func tabBar(tabBar: UITabBar, didSelectItem item: UITabBarItem) {
print("Selected item")
}
Function from the UITabBarDelegate. But obviously we can't do this in SwiftUI so was looking to see if there was any ideas people have tried. My last thought would be to simply wrap it in a UIView and use it with SwiftUI but would like to avoid this and keep it native.
I have seen a write up in a custom TabBar but would like to use the TabBar provided by Apple to avoid any future discrepancies.
Thanks!
Edit: Make the question clearer.
Thanks to Aleskey for the great answer (Marked as correct). I evolved it a little bit in addition to a medium article that was written around a Modal. I found it to be a little different
Here's the jist.
A MainTabBarData which is an Observable Object
final class MainTabBarData: ObservableObject {
/// This is the index of the item that fires a custom action
let customActiontemindex: Int
let objectWillChange = PassthroughSubject<MainTabBarData, Never>()
var previousItem: Int
var itemSelected: Int {
didSet {
if itemSelected == customActiontemindex {
previousItem = oldValue
itemSelected = oldValue
isCustomItemSelected = true
}
objectWillChange.send(self)
}
}
func reset() {
itemSelected = previousItem
objectWillChange.send(self)
}
/// This is true when the user has selected the Item with the custom action
var isCustomItemSelected: Bool = false
init(initialIndex: Int = 1, customItemIndex: Int) {
self.customActiontemindex = customItemIndex
self.itemSelected = initialIndex
self.previousItem = initialIndex
}
}
And this is the TabbedView
struct TabbedView: View {
#ObservedObject private var tabData = MainTabBarData(initialIndex: 1, customItemIndex: 2)
var body: some View {
TabView(selection: $tabData.itemSelected) {
Text("First Screen")
.tabItem {
VStack {
Image(systemName: "globe")
.font(.system(size: 22))
Text("Profile")
}
}.tag(1)
Text("Second Screen")
.tabItem {
VStack {
Image(systemName: "plus.circle")
.font(.system(size: 22))
Text("Profile")
}
}.tag(2)
Text("Third Screen")
.tabItem {
VStack {
Image(systemName: "number")
.font(.system(size: 22))
Text("Profile")
}
}.tag(3)
}.actionSheet(isPresented: $tabData.isCustomItemSelected) {
ActionSheet(title: Text("SwiftUI ActionSheet"), message: Text("Action Sheet Example"),
buttons: [
.default(Text("Option 1"), action: option1),
.default(Text("Option 2"), action: option2),
.cancel(cancel)
]
)
}
}
func option1() {
tabData.reset()
// ...
}
func option2() {
tabData.reset()
// ...
}
func cancel() {
tabData.reset()
}
}
struct TabbedView_Previews: PreviewProvider {
static var previews: some View {
TabbedView()
}
}
Similar concept, just uses the power of SwiftUI and Combine.
You could introduce new #State property for storing old tag of presented tab. And perform the next method for each of your tabs .onAppear { self.oldSelectedItem = self.selectedItem } except the middle tab. The middle tab will be responsible for showing the action sheet and its method will look the following:
.onAppear {
self.shouldShowActionSheet.toggle()
self.selectedItem = self.oldSelectedItem
}
Working example:
import SwiftUI
struct ContentView: View {
#State private var selectedItem = 1
#State private var shouldShowActionSheet = false
#State private var oldSelectedItem = 1
var body: some View {
TabView (selection: $selectedItem) {
Text("Home")
.tabItem { Image(systemName: "house") }
.tag(1)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Search")
.tabItem { Image(systemName: "magnifyingglass") }
.tag(2)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Add")
.tabItem { Image(systemName: "plus.circle") }
.tag(3)
.onAppear {
self.shouldShowActionSheet.toggle()
self.selectedItem = self.oldSelectedItem
}
Text("Heart")
.tabItem { Image(systemName: "heart") }
.tag(4)
.onAppear { self.oldSelectedItem = self.selectedItem }
Text("Profile")
.tabItem { Image(systemName: "person.crop.circle") }
.tag(5)
.onAppear { self.oldSelectedItem = self.selectedItem }
}
.actionSheet(isPresented: $shouldShowActionSheet) { ActionSheet(title: Text("Title"), message: Text("Message"), buttons: [.default(Text("Option 1"), action: option1), .default(Text("Option 2"), action: option2) , .cancel()]) }
}
func option1() {
// do logic 1
}
func option2() {
// do logic 2
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Previous answers did not help me so I'm pasting my complete solution.
import SwiftUI
import UIKit
enum Tab {
case map
case recorded
}
#main
struct MyApp: App {
#State private var selectedTab: Tab = .map
#Environment(\.scenePhase) private var phase
var body: some Scene {
WindowGroup {
VStack {
switch selectedTab {
case .map:
NavigationView {
FirstView()
}
case .recorded:
NavigationView {
SecondView()
}
}
CustomTabView(selectedTab: $selectedTab)
.frame(height: 50)
}
}
}
}
struct FirstView: View {
var body: some View {
Color(.systemGray6)
.ignoresSafeArea()
.navigationTitle("First view")
}
}
struct SecondView: View {
var body: some View {
Color(.systemGray6)
.ignoresSafeArea()
.navigationTitle("second view")
}
}
struct CustomTabView: View {
#Binding var selectedTab: Tab
var body: some View {
HStack {
Spacer()
Button {
selectedTab = .map
} label: {
VStack {
Image(systemName: "map")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
Text("Map")
.font(.caption2)
}
.foregroundColor(selectedTab == .map ? .blue : .primary)
}
.frame(width: 60, height: 50)
Spacer()
Button {
} label: {
ZStack {
Circle()
.foregroundColor(.secondary)
.frame(width: 80, height: 80)
.shadow(radius: 2)
Image(systemName: "plus.circle.fill")
.resizable()
.foregroundColor(.primary)
.frame(width: 72, height: 72)
}
.offset(y: -2)
}
Spacer()
Button {
selectedTab = .recorded
} label: {
VStack {
Image(systemName: "chart.bar")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
Text("Recorded")
.font(.caption2)
}
.foregroundColor(selectedTab == .recorded ? .blue : .primary)
}
.frame(width: 60, height: 50)
Spacer()
}
}
}