How to make a left vertical tabView on SwiftUI TVOS? - swiftui

Whenever a tabButton is highlighted, I wanna show the corresponding content on the RightMainView, I can use a #Published property on ViewModel to do that, but the problem is that the same RightMainView will be redraw while switching tabs.
The MainView will be a complicated UI and also has focus engine, so I definitely do not want the MainView redraw.
import SwiftUI
struct Model: Identifiable, Equatable {
let id = UUID()
let title: String
static func == (lhs: Model, rhs: Model) -> Bool {
return lhs.title == rhs.title
}
}
class ViewModel: ObservableObject {
let titles = [Model(title: "Home"), Model(title: "live"), Model(title: "setting"), Model(title: "network")]
#Published var selected: Model = Model(title: "Home")
}
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
HStack(spacing: 0) {
leftTab
.focusSection()
rightMainView
}.edgesIgnoringSafeArea(.all)
}
private var leftTab: some View {
VStack {
ForEach(viewModel.titles, id: \.self.id) { title in
ZStack {
TabButton(viewModel: viewModel, title: title)
}.focusable()
}
}
.frame(maxWidth: 400, maxHeight: .infinity)
.background(.yellow)
}
private var rightMainView: some View {
VStack {
let _ = print("Redrawing the View")
Text(viewModel.selected.title)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.red)
}
}
}
struct TabButton: View {
#Environment(\.isFocused) var isFocused
let viewModel: ViewModel
let title: Model
var body: some View {
Text(title.title)
.frame(width: 200)
.padding(30)
.background(isFocused ? .orange : .white)
.foregroundColor(isFocused ? .black : .gray)
.cornerRadius(20)
.onChange(of: isFocused) { newValue in
if newValue {
viewModel.selected = title
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here are some other things I have tried:
I tried the native TabView, https://developer.apple.com/documentation/swiftui/tabview, the View does not redrew while switch tabs, but it only support horizontal, not left vertical model, is there a way I can use native TabView to implement my vertical tabView (with both text and images)
I tried the NavigationSplitView as well, https://developer.apple.com/documentation/swiftui/navigationsplitview, the behavior is not the same on TVOS with iPad tho. On TVOS only the TabButtons are showing, the MainView/details are not showing

Related

Pushing multiple navigation links from a parent view in SwiftUI

I want to implement a wizard whereby the user has to go through multiple screens in order to complete a signup process.
In SwiftUI the easiest way to do this is to have each view when it's finished push the next view on the navigation stack, but this codes the entire navigation between views in the views themselves, and I would like to avoid it.
What I want to do is have a parent view show the navigation view and then push the different steps on that navigation view.
I have something working already that looks like this:
struct AddVehicleView: View {
#ObservedObject var viewModel: AddVehicleViewModel
var body: some View {
NavigationView {
switch viewModel.state {
case .description:
AddDescriptionView(addDescriptionViewModel: AddVehicleDescriptionViewModel(), addVehicleViewModel: viewModel)
case .users:
AddUsersView(viewModel: AddUsersViewModel(viewModel.vehicle), addVehicleViewModel: viewModel)
}
}
}
}
This works fine. In the first step the AddVehicleViewModel is updated with the necessary info, the AddVehicleView is re-evaluated, the switch case jumps to the next option and the next view is presented to complete the wizard.
The issue with this however is that there are no navigation stack animations. Views simply get replaced. How can I change this to a system whereby the views are pushed, without implementing the push inside the AddDescriptionView object?
Should I write wrapper views that do the navigation stack handling on top of those views, and get rid of the switch case?
Ok so if you want to go from view a to b you should implement this not in your NavigationView but the view after the NavigationView, this way you wont break the animations. Why? Good question, I really don't know. When possible I keep my NavigationView always in the App struct under WindowGroup.
To get back to the point. Basically there should be an intermediate view between your steps and NavigationView. This view (StepperView) will contain the navigation logic of your steps. This way you keep the animations intact.
import SwiftUI
class AddVehicleViewModel: ObservableObject {
enum StateType {
case description
case users1
case users2
}
#Published var state: StateType? = nil
}
struct AddDescriptionView: View {
#ObservedObject var viewModel: AddVehicleViewModel
#State var text: String = ""
var body: some View {
GeometryReader {proxy in
VStack {
TextField("test", text: self.$text).background(RoundedRectangle(cornerRadius: 10).fill(Color.white).frame(width: 150, height: 40)).padding()
Button("1") {
viewModel.state = .users1
}
}.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center).background(Color.orange)
}
}
}
struct AddUsersView: View {
#ObservedObject var viewModel: AddVehicleViewModel
var body: some View {
GeometryReader {proxy in
ZStack {
Button("2") {
viewModel.state = .users2
}
}.frame(width: proxy.size.width, height: proxy.size.height, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/).background(Color.orange)
}
}
}
struct AddUsersView2: View {
#ObservedObject var viewModel: AddVehicleViewModel
var body: some View {
GeometryReader {proxy in
ZStack {
Button("3") {
viewModel.state = .description
}
}.frame(width: proxy.size.width, height: proxy.size.height, alignment: /*#START_MENU_TOKEN#*/.center/*#END_MENU_TOKEN#*/).background(Color.orange)
}
}
}
struct StepperView: View {
#ObservedObject var viewModel: AddVehicleViewModel = AddVehicleViewModel()
var body: some View {
VStack {
NavigationLink(
destination: AddDescriptionView(viewModel: viewModel),
isActive: .constant(viewModel.state == .description),
label: {EmptyView()})
if viewModel.state == .users1 {
NavigationLink(
destination: AddUsersView(viewModel: viewModel),
isActive: .constant(true),
label: {EmptyView()})
}
if viewModel.state == .users2 {
NavigationLink(
destination: AddUsersView2(viewModel: viewModel),
isActive: .constant(true),
label: {EmptyView()})
}
}.onAppear {
viewModel.state = .description
}
}
}
class BackBarButtonItem: UIBarButtonItem {
#available(iOS 14.0, *)
override var menu: UIMenu? {
set {
// Don't set the menu here
// super.menu = menu
}
get {
return super.menu
}
}
}
struct AddVehicleView: View {
#ObservedObject var viewModel: AddVehicleViewModel = AddVehicleViewModel()
var body: some View {
NavigationView {
NavigationLink(
destination: StepperView(),
isActive: .constant(true),
label: {EmptyView()})
}
}
}

How to make a Dynamic PageViewController in SwiftUI?

Anyone know how to make a dynamic pageView controller in SwiftUI, iOS 14? Something that displays pages that are a function of their date so that one can scroll left or right to look at data from the past, present and future.
struct DatePg: View
{
let date: Date
var body: some View {
Text(date.description)
}
}
There is a new API that allows one to make a PageViewController with the TabView and a viewModifier. But the only examples I've seen are static. Here's an example of a static PageView.
import SwiftUI
struct SwiftUIPageView: View
{
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
Text("Hello")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue)
.tag(0)
Text("World")
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.tag(1)
}.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
Already have something working using UIHostingController but passing NSManageObjectContext through the UIKit objects is cuasing problems.
Here's where I'm at so far. Still not working.
import SwiftUI
#main struct PagerApp: App
{
var body: some Scene {
WindowGroup { DatePageView() }
}
}
struct DatePageView: View
{
#StateObject var dateData = DateData(present: Date())
#State var index: Int = 1
var body: some View {
TabView(selection: $index) {
ForEach(dateData.dates, id: \.self) { date in
Text(date.description)
.onAppear { dateData.current(date: date) }
.tag(dateData.tag(date: date))
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
}
}
class DateData: ObservableObject
{
#Published var dates: [Date]
init(present: Date) {
let past = present.previousDay()
let future = present.nextDay()
self.dates = [past, present, future]
}
func current(date: Date) {
//center around
guard let i = dates.firstIndex(of: date) else { fatalError() }
self.dates = [ dates[i].previousDay(), dates[i], dates[i].nextDay() ]
print("make item at \(i) present")
}
func tag(date: Date) -> Int {
guard let i = dates.firstIndex(of: date) else { fatalError() }
return i
}
}
You can create view dynamically using an array and ForEach.
Here is an example using an array of strings:
// See edited section
You could pass the items you want in the View initializer
Edit:
Here is an example for adding a new page each time I reach the last one:
struct SwiftUIPageView: View
{
#State private var selection = "0"
#State var items: [String] = ["0", "1", "2", "3"]
var body: some View {
TabView(selection: $selection) {
ForEach(items, id: \.self) { item in
Text(item)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.red)
.tag(item)
}
}.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.onChange(of: selection, perform: { value in
if Int(value) == (self.items.count - 1) {
self.items.append("\(self.items.count)")
}
})
.id(items)
}
}
The last id(items) is important because it forces the View to reload when the array changes.

SwiftUI drag and drop on moving/animating view/shape

I am trying to implement drag and drop feature in SwiftUI. It seems when view is not animating the following code works fine. But when I am moving the view with offset then there is some issue with drag location and view like view location is not same as touch location and view shape is not same as original.
Dragging without offset animation(animate = false) works fine:
Dragging with offset animation(animate = true):
Code Snippet:
import SwiftUI
struct ContentView: View {
#ObservedObject var dropDelegate = CircleDropDelegate()
#State var animate = false
var body: some View {
VStack(alignment: .leading) {
Spacer()
Circle()
.fill(Color.green)
.offset(x: animate ? 100 : 0)
.animation(Animation.linear(duration: 5).repeatForever(autoreverses: true))
.frame(width: 50, height: 50)
.onDrag({
return NSItemProvider(object: "circle" as NSString)
})
Spacer()
Rectangle()
.fill(Color.green)
.frame(height: 150)
.onDrop(of: ["public.text"], delegate: dropDelegate)
Spacer()
}
.onAppear {
animate = true
}
.padding()
.background(Color.init(white: 0.7))
}
}
class CircleDropDelegate: ObservableObject, DropDelegate {
#Published var isEntered: Bool = false
func performDrop(info: DropInfo) -> Bool {
print("perform drop on box \(info)")
return true
}
func dropEntered(info: DropInfo) {
print("circle entered the box")
isEntered = true
}
func dropExited(info: DropInfo) {
print("circle exited the box")
isEntered = false
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Is there a way to fix this issue?

How do I change button backgroundcolor if the button is disabled in swiftUI

I'm trying to create a buttonStyle that has a different background color if the button is disabled.
How do I do that?
I've created the code below to react to a variable that I've introduced myself, but is it possible to have it react on the buttons .disabled() state?
My code:
struct MyButtonStyle: ButtonStyle {
var enabledState = false
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.foregroundColor(Color.white)
.padding(10)
.padding(.horizontal, 20)
.background(self.enabledState ? Color(UIColor.orange) : Color(UIColor.lightGray))
.cornerRadius(20)
.frame(minWidth: 112, idealWidth: 112, maxWidth: .infinity, minHeight: 40, idealHeight: 40, maxHeight: 40, alignment: .center)
.scaleEffect(configuration.isPressed ? 0.9 : 1.0)
}
}
struct ContentView: View {
#State private var buttonEnabled = false
var body: some View {
HStack {
Button("Button") {
self.buttonEnabled.toggle()
print("Button pressed")
}
}
.buttonStyle(MyButtonStyle(enabledState: self.buttonEnabled))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
To track .disabled there is EnvironmentValues.isEnabled that shows this state. But environment values are applicable only to views and do not work in style.
So the solution is to create custom button that tracks isEnabled and pass it into own style.
Below is a demo of solution approach (MyButtonStyle is not changed). Tested with Xcode 12b.
struct MyButton: View {
let title: String
let action: () -> ()
#Environment(\.isEnabled) var isEnabled // to handle own state !!
init(_ title: String, action: #escaping () -> ()) {
self.title = title
self.action = action
}
var body: some View {
Button(title, action: action)
.buttonStyle(MyButtonStyle(enabledState: isEnabled))
}
}
struct ContentView: View {
#State private var buttonEnabled = true
var body: some View {
HStack {
MyButton("Button") { // << here !!
self.buttonEnabled.toggle()
print("Button pressed")
}
.disabled(!buttonEnabled) // << here !!
}
}
}
Again using the Environment.isEnabled but this time using a ViewModifier. This has the advantage that you can use it on other Views, not just Buttons. This implementation reduces the opacity of the button background color so no need for a new style to be injected.
struct MyButtonModifier: ViewModifier {
#Environment(\.isEnabled) var isEnabled
let backgroundColor: Color
func body(content: Content) -> some View {
content
.background(backgroundColor.opacity(isEnabled ? 1 : 0.5))
}
}
Then use it in your code as
Button("foo") {
// action
}
.modifier(MyButtonModifier(backgroundColor: Color.red))

InputAccessoryView / View Pinned to Keyboard with SwiftUI

Is there an equivalent to InputAccessoryView in SwiftUI (or any indication one is coming?)
And if not, how would you emulate the behavior of an InputAccessoryView (i.e. a view pinned to the top of the keyboard)? Desired behavior is something like iMessage, where there is a view pinned to the bottom of the screen that animates up when the keyboard is opened and is positioned directly above the keyboard. For example:
Keyboard closed:
Keyboard open:
iOS 15.0+
macOS 12.0+,Mac Catalyst 15.0+
ToolbarItemPlacement has a new property in iOS 15.0+
keyboard
On iOS, keyboard items are above the software keyboard when present, or at the bottom of the screen when a hardware keyboard is attached.
On macOS, keyboard items will be placed inside the Touch Bar.
https://developer.apple.com
struct LoginForm: View {
#State private var username = ""
#State private var password = ""
var body: some View {
Form {
TextField("Username", text: $username)
SecureField("Password", text: $password)
}
.toolbar(content: {
ToolbarItemGroup(placement: .keyboard, content: {
Text("Left")
Spacer()
Text("Right")
})
})
}
}
iMessage like InputAccessoryView in iOS 15+.
struct KeyboardToolbar<ToolbarView: View>: ViewModifier {
private let height: CGFloat
private let toolbarView: ToolbarView
init(height: CGFloat, #ViewBuilder toolbar: () -> ToolbarView) {
self.height = height
self.toolbarView = toolbar()
}
func body(content: Content) -> some View {
ZStack(alignment: .bottom) {
GeometryReader { geometry in
VStack {
content
}
.frame(width: geometry.size.width, height: geometry.size.height - height)
}
toolbarView
.frame(height: self.height)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
extension View {
func keyboardToolbar<ToolbarView>(height: CGFloat, view: #escaping () -> ToolbarView) -> some View where ToolbarView: View {
modifier(KeyboardToolbar(height: height, toolbar: view))
}
}
And use .keyboardToolbar view modifier as you would normally do.
struct ContentView: View {
#State private var username = ""
var body: some View {
NavigationView{
Text("Keyboar toolbar")
.keyboardToolbar(height: 50) {
HStack {
TextField("Username", text: $username)
}
.border(.secondary, width: 1)
.padding()
}
}
}
}
I got something working which is quite near the wanted result. So at first, it's not possible to do this with SwiftUI only. You still have to use UIKit for creating the UITextField with the wanted "inputAccessoryView". The textfield in SwiftUI doesn't have the certain method.
First I created a new struct:
import UIKit
import SwiftUI
struct InputAccessory: UIViewRepresentable {
func makeUIView(context: Context) -> UITextField {
let customView = UIView(frame: CGRect(x: 0, y: 0, width: 10, height: 44))
customView.backgroundColor = UIColor.red
let sampleTextField = UITextField(frame: CGRect(x: 20, y: 100, width: 300, height: 40))
sampleTextField.inputAccessoryView = customView
sampleTextField.placeholder = "placeholder"
return sampleTextField
}
func updateUIView(_ uiView: UITextField, context: Context) {
}
}
With that I could finally create a new textfield in the body of my view:
import SwiftUI
struct Test: View {
#State private var showInput: Bool = false
var body: some View {
HStack{
Spacer()
if showInput{
InputAccessory()
}else{
InputAccessory().hidden()
}
}
}
}
Now you can hide and show the textfield with the "showInput" state. The next problem is, that you have to open your keyboard at a certain event and show the textfield. That's again not possible with SwiftUI and you have to go back to UiKit and making it first responder. If you try my code, you should see a red background above the keyboard. Now you only have to move the field up and you got a working version.
Overall, at the current state it's not possible to work with the keyboard or with the certain textfield method.
I've solved this problem using 99% pure SwiftUI on iOS 14.
In the toolbar you can show any View you like.
That's my implementation:
import SwiftUI
struct ContentView: View {
#State private var showtextFieldToolbar = false
#State private var text = ""
var body: some View {
ZStack {
VStack {
TextField("Write here", text: $text) { isChanged in
if isChanged {
showtextFieldToolbar = true
}
} onCommit: {
showtextFieldToolbar = false
}
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
VStack {
Spacer()
if showtextFieldToolbar {
HStack {
Spacer()
Button("Close") {
showtextFieldToolbar = false
UIApplication.shared
.sendAction(#selector(UIResponder.resignFirstResponder),
to: nil, from: nil, for: nil)
}
.foregroundColor(Color.black)
.padding(.trailing, 12)
}
.frame(idealWidth: .infinity, maxWidth: .infinity,
idealHeight: 44, maxHeight: 44,
alignment: .center)
.background(Color.gray)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I managed to create a nicely working solution with some help from this post by Swift Student, with quite a lot of modification & addition of functionality you take for granted in UIKit. It is a wrapper around UITextField, but that's completely hidden from the user and it's very SwiftUI in its implementation. You can take a look at it in my GitHub repo - and you can bring it into your project as a Swift Package.
(There's too much code to put it in this answer, hence the link to the repo)
I have a implementation that can custom your toolbar
public struct InputTextField<Content: View>: View {
private let placeholder: LocalizedStringKey
#Binding
private var text: String
private let onEditingChanged: (Bool) -> Void
private let onCommit: () -> Void
private let content: () -> Content
#State
private var isShowingToolbar: Bool = false
public init(placeholder: LocalizedStringKey = "",
text: Binding<String>,
onEditingChanged: #escaping (Bool) -> Void = { _ in },
onCommit: #escaping () -> Void = { },
#ViewBuilder content: #escaping () -> Content) {
self.placeholder = placeholder
self._text = text
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
self.content = content
}
public var body: some View {
ZStack {
TextField(placeholder, text: $text) { isChanged in
if isChanged {
isShowingToolbar = true
}
onEditingChanged(isChanged)
} onCommit: {
isShowingToolbar = false
onCommit()
}
.textFieldStyle(RoundedBorderTextFieldStyle())
VStack {
Spacer()
if isShowingToolbar {
content()
}
}
}
}
}
You can do it this way without using a UIViewRepresentable.
Its based on https://stackoverflow.com/a/67502495/5718200
.onReceive(NotificationCenter.default.publisher(for: UITextField.textDidBeginEditingNotification)) { notification in
if let textField = notification.object as? UITextField {
let yourAccessoryView = UIToolbar()
// set your frame, buttons here
textField.inputAccessoryView = yourAccessoryView
}
}
}