How can I create a predicate so that when the user selects "Full Body" it returns the entire list with no predicate? Right now, it is returning "part" which corresponds to the muscle groups I have set (Abs, Legs, Push, Pull). I want to return all of the options when "Full Body" is selected. How could I write an If statement so that the predicate is not used?
import SwiftUI
var parts = ["Abs", "Legs", "Push", "Pull", "Full Body"]
struct ExerciseList: View {
#State private var selectedPart = " "
var body: some View {
NavigationView {
VStack (alignment: .leading) {
NavigationLink(destination: AddExerciseView()){
Text("Add Exercise")
.fontWeight(.bold)
}
Picker("Body Part", selection: $selectedPart) {
ForEach(parts, id:\.self) { part in
Text(part)
}
}.pickerStyle(.segmented)
ListView(part:selectedPart)
}
}
}
}
import SwiftUI
struct ListView: View {
var part: String
#FetchRequest var exercises: FetchedResults<Exercise>
init(part: String) {
self.part = part
self._exercises = FetchRequest(
entity: Exercise.entity(),
sortDescriptors: [],
predicate: NSPredicate(format: "musclegroup == %#", part as any CVarArg)
)
}
var body: some View {
List(exercises) { e in
Text(e.exercisename)
}
}
}
It's not a good idea to init objects inside View structs because the heap allocation slows things down. You could either have all the predicates created before hand or create one when the picker value changes, e.g. something like this:
// all the Picker samples in the docs tend to use enums.
enum Part: String, Identifiable, CaseIterable {
case abs
case legs
case push
case pull
case fullBody
var id: Self { self }
// Apple sometimes does it like this
// var localizedName: LocalizedStringKey {
// switch self {
// case .abs: return "Abs"
// case .legs: return "Legs"
// case .push: return "Push"
// case .pull: return "Pull"
// case .fullBody: return "Full Body"
// }
// }
}
struct ExerciseListConfig {
var selectedPart: Part = .fullBody {
didSet {
if selectedPart == .fullBody {
predicate = nil
}
else {
// note this will use the lower case string
predicate = NSPredicate(format: "musclegroup == %#", selectedPart.rawValue)
}
}
}
var predicate: NSPredicate?
}
struct ExerciseList: View {
#State private var config = ExerciseListConfig()
var body: some View {
NavigationView {
VStack (alignment: .leading) {
Picker("Body Part", selection: $config.selectedPart) {
//ForEach(Part.allCases) // Apple sometimes does this but means you can't easily change the display order.
Text("Abs").tag(Part.abs)
Text("Legs").tag(Part.legs)
Text("Push").tag(Part.push)
Text("Pull").tag(Part.pull)
Text("Full Body").tag(Part.fullBody)
}.pickerStyle(.segmented)
ExerciseListView(predicate:config.predicate)
}
}
}
}
struct ExerciseListView: View {
// var part: String
let predicate: NSPredicate?
// #FetchRequest var exercises: FetchedResults<Exercise>
init(predicate: NSPredicate?) {
self.predicate = predicate
// self._exercises = FetchRequest(
// entity: Exercise.entity(),
// sortDescriptors: [],
//
// predicate: NSPredicate(format: "musclegroup == %#", part as any CVarArg)
// )
}
var body: some View {
Text(predicate?.description ?? "")
// List(exercises) { e in
// Text(e.exercisename)
// }
}
}
Since you are using Core Data you might want to use an Int enum in the entity for less storage and faster queries.
you could try this simple approach in ListView:
init(part: String) {
self.part = part
self._exercises = FetchRequest(
entity: Exercise.entity(),
sortDescriptors: [],
predicate: (part == "Full Body")
? nil
: NSPredicate(format: "musclegroup == %#", part as any CVarArg)
)
}
Related
I have the following code:
import SwiftUI
enum OptionButtonRole {
case normal
case destructive
case cancel
}
struct OptionButton {
var title: String
var role: OptionButtonRole
let id = UUID()
var action: () -> Void
}
struct OptionSheet: ViewModifier {
#State var isPresented = true
var title: String
var buttons: [OptionButton]
init(title: String, buttons: OptionButton...) {
self.title = title
self.buttons = buttons
}
func body(content: Content) -> some View {
content
.confirmationDialog(title,
isPresented: $isPresented,
titleVisibility: .visible) {
ForEach(buttons, id: \.title) { button in
let role: ButtonRole? = button.role == .normal ? nil : button.role == .destructive ? .destructive : .cancel
Button(button.title, role: role, action: button.action)
}
}
}
}
It builds and my app shows the option sheet with the specified buttons.
However, if I use an alternative Button.init, i.e. if I replace the body with the following code:
func body(content: Content) -> some View {
content
.confirmationDialog(title,
isPresented: $isPresented,
titleVisibility: .visible) {
ForEach(buttons, id: \.title) { button in
let role: ButtonRole? = button.role == .normal ? nil : button.role == .destructive ? .destructive : .cancel
Button(role: role, action: button.action) {
Text(button.title)
}
}
}
}
Then, Xcode hangs on build with the following activity:
Is there an error in my code or is this a compiler bug (Xcode Version 14.1 (14B47b))?
While your code is technically correct, the ability of view logic to evaluate variable values can get quite compiler-intensive, especially when you have multiple chained ternaries and logic inside a ForEach (which seems to make a bigger difference than one would probably think).
I'd be tempted to move the conditional logic outside the loop altogether, so that you're calling a method rather than needing to evaluate and store a local variable. You could make this a private func in your view, or as an extension to your custom enum. For example:
extension OptionButtonRole {
var buttonRole: ButtonRole? {
switch self {
case .destructive: return .destructive
case .cancel: return .cancel
default: return nil
}
}
}
// in your view
ForEach(buttons, id: \.title) { button in
Button(role: button.role.buttonRole, action: button.action) {
Text(button.title)
}
}
SwiftUI newbie here. 👋 I'm struggling to understand how #FocusedBinding and FocusedValues work when building menus for a MacOS app. I'm trying to build the Apple HIG UI pattern with buttons in the window toolbar for changing the list view, and matching menu items in the View menu. Much like Finder windows have the four different view modes.
I have gone through the Apple's Landmarks tutorial, the Frameworks Engineer's example code in Apple dev forum, and Majid's tutorial.
The Apple documentation says FocusedValues is "a collection of state exported by the focused view and its ancestors." I assume the collection is global and I can set a focusedValue in any child View, and read or bind to any of the FocusedValues from anywhere in my code.
Therefore I don't understand why my first example below works, but the second one doesn't?
This works:
import SwiftUI
#main
struct TestiApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.frame(minWidth: 200, minHeight: 300)
.onAppear {
NSWindow.allowsAutomaticWindowTabbing = false
}
}
.windowStyle(HiddenTitleBarWindowStyle())
.commands {
MenuCommands()
}
}
}
struct ContentView: View {
// In the working version of the code selectedView is defined here
// in the ContentView, which is a direct child of the WindowGroup
// that has the .commands modifier.
#State private var selectedView: Int = 0
// For demonstration purposes I have simplified the authorization
// code to a hardcoded boolean.
private var isAuthorized: Bool = true
var body: some View {
switch isAuthorized {
case true:
// focusedValue is set here to the selectedView binding.
// I don't really understand why do it here, but it works.
AuthorizedView(selectedView: $selectedView)
.focusedValue(\.selectedViewBinding, $selectedView)
default:
NotYetAuthorizedView()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct AuthorizedView: View {
// selectedView is passed to this view as an argument and
// bound from ContentView.
#Binding var selectedView: Int
var body: some View {
List {
Text("Something here")
Text("Something more")
Text("Even more")
}
.toolbar {
// The Picker element sets and gets the bound selectedView value
Picker("View", selection: $selectedView) {
Text("View 1").tag(0)
Text("View 2").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
struct NotYetAuthorizedView: View {
var body: some View {
VStack {
Text("You need permission to access this 😭")
.padding()
}
}
}
struct MenuCommands: Commands {
private struct MenuContent: View {
// Command menu binds the selectedView value through focusedValues.
// MenuContent is a View, because otherwise the binding doesn't
// work (I read there's a bug in SwiftUI...).
#FocusedBinding(\.selectedViewBinding) var selectedView: Int?
var body: some View {
Button("View 1") {
selectedView = 0
}
.keyboardShortcut("1")
Button("View 2") {
selectedView = 1
}
.keyboardShortcut("2")
}
}
var body: some Commands {
CommandGroup(before: .toolbar) {
MenuContent()
}
}
}
struct SelectedViewBinding: FocusedValueKey {
typealias Value = Binding<Int>
}
extension FocusedValues {
var selectedViewBinding: SelectedViewBinding.Value? {
get { self[SelectedViewBinding.self] }
set { self[SelectedViewBinding.self] = newValue }
}
}
But if I make the following changes to ContentView and AuthorizedView, the project compiles fine but the binding between selectedView and the command menus no longer works:
struct ContentView: View {
// selectedView definition has been removed from ContentView
// and moved to AuthorizedView.
private var isAuthorized: Bool = true
var body: some View {
switch isAuthorized {
case true:
AuthorizedView()
// Also setting the focusedValue here has been removed
default:
NotYetAuthorizedView()
}
}
}
struct AuthorizedView: View {
// Moved selectedView definition here
#State private var selectedView: Int = 0
var body: some View {
List {
Text("Something here")
Text("Something more")
Text("Even more")
}
.toolbar {
Picker("View", selection: $selectedView) {
Text("View 1").tag(0)
Text("View 2").tag(1)
}
.pickerStyle(SegmentedPickerStyle())
.focusedValue(\.selectedViewBinding, $selectedView)
// I am now setting the focusedValue here, which seems
// more logical to me...
}
}
}
I like the second example better, because the selectedView state is encapsulated in the AuthorizedView where it belongs.
I am trying to present a sequence of Views, each gathering some information from the user. When users enter all necessary data, they can move to next View. So far I have arrived at this (simplified) code, but I am unable to display the subview itself (see first line in MasterView VStack{}).
import SwiftUI
protocol DataEntry {
var entryComplete : Bool { get }
}
struct FirstSubView : View, DataEntry {
#State var entryComplete: Bool = false
var body: some View {
VStack{
Text("Gender")
Button("Male") {
entryComplete = true
}
Button("Female") {
entryComplete = true
}
}
}
}
struct SecondSubView : View, DataEntry {
var entryComplete: Bool {
return self.name != ""
}
#State private var name : String = ""
var body: some View {
Text("Age")
TextField("Your name", text: $name)
}
}
struct MasterView: View {
#State private var currentViewIndex = 0
let subview : [DataEntry] = [FirstSubView(), SecondSubView()]
var body: some View {
VStack{
//subview[currentViewIndex]
Text("Subview placeholder")
Spacer()
HStack {
Button("Prev"){
if currentViewIndex > 0 {
currentViewIndex -= 1
}
}.disabled(currentViewIndex == 0)
Spacer()
Button("Next"){
if (currentViewIndex < subview.count-1){
currentViewIndex += 1
}
}.disabled(!subview[currentViewIndex].entryComplete)
}
}
}
}
I do not want to use NavigationView for styling reasons. Can you please point me in the right direction how to solve this problem? Maybe a different approach?
One way to do this is with a Base View and a switch statement combined with an enum. This is a similar pattern I've used in the past to separate flows.
enum SubViewState {
case ViewOne, ViewTwo
}
The enum serves as a way to easily remember and track which views you have available.
struct BaseView: View {
#EnvironmentObject var subViewState: SubViewState = .ViewOne
var body: some View {
switch subViewState {
case ViewOne:
ViewOne()
case ViewTwo:
ViewTwo()
}
}
}
The base view is a Container for the view control. You will likely add a view model, which is recommended, and set the state value for your #EnvironmentObject or you'll get a null pointer exception. In this example I set it, but I'm not 100% sure if that syntax is correct as I don't have my IDE available.
struct SomeOtherView: View {
#EnvironmentObject var subViewState: SubViewState
var body: some View {
BaseView()
Button("Switch View") {
subViewState = .ViewTwo
}
}
}
This is just an example of using it. You can access your #EnvironmentObject from anywhere, even other views, as it's always available until disposed of. You can simply set a new value to it and it will update the BaseView() that is being shown here. You can use the same principle in your code, using logic, to determine the view to be shown and simply set its value and it will update.
I'm trying to bind to an enum instance's rawValue
enum Brand: Int, CaseIterable, Codable {
case toyota = 0
case mazda
case suzuki
}
struct Car {
var brand: Brand
}
struct CarView: View {
#Binding car: Car
var body: some View {
SomeView(selectionInt: $car.brand)
}
}
But I get this error:
Cannot convert value of type 'Binding' to expected argument
type 'Binding'.
If've tried using $car.brand.rawValue instead (in SomeView's parameters) but then I get this error:
Cannot assign to property: 'rawValue' is immutable.
How can i bind my View to the model instance's rawValue?
Using a brandProxy to maintain the integrity of the other Car variables
import SwiftUI
enum Brand: Int, CaseIterable, Codable {
case toyota = 0
case mazda
case suzuki
case unknown
func description() -> String {
var result = "unknown"
switch self {
case .toyota:
result = "toyota"
case .mazda:
result = "mazda"
case .suzuki:
result = "suzuki"
case .unknown:
result = "unknown"
}
return result
}
}
struct CarView: View {
//This makes it mutable
#State var car: Car
var brandProxy: Binding<Int> {
Binding<Int>(
get: {
car.brand.rawValue
},
set: {
car.brand = Brand(rawValue: $0) ?? Brand.unknown
}
)
}
var body: some View {
VStack{
Text(car.brand.description())
SomeView(selectedIndex: brandProxy)
}
}
}
For this kind of binding you need a custom Binding to help SwiftUI to Bind data!
Info: You can not change rawValue of enum because they are immutable!
and also try of changing them in way of Binding would not help you, they are immutable! you can get them not set them!
So with that info you can know why SwiftUI does not want Bind it at fist place! because if SwiftUI make that Binding possible that would be a BIG violation of enum rawValues! then we will try use Binding power in this way that we change the Int of car not Enum. see the code for example.
import SwiftUI
struct ContentView: View {
#State private var selectedCar: Car = Car(brand: Brand.suzuki)
var body: some View {
VStack {
Picker("", selection: $selectedCar.brand) {
ForEach(Brand.allCases, id: \.self) { car in
Text(car.description)
}
}
.pickerStyle(SegmentedPickerStyle())
}
.padding()
CarView(selectionInt: Binding.init(get: { () -> Int in return selectedCar.brand.rawValue },
set: { newValue in
if let unwrappedBrand = Brand(rawValue: newValue) { selectedCar = Car(brand: unwrappedBrand) } }) )
}
}
struct CarView: View {
#Binding var selectionInt: Int
var body: some View {
Text("selected Int: " + selectionInt.description)
.padding()
let selectedCar = (Brand(rawValue: selectionInt)?.description ?? "unknown")
Text( "selected car: " + selectedCar)
.padding()
VStack(spacing: 20.0) {
Button("select toyota") { selectionInt = 0 }
Button("select mazda") { selectionInt = 1 }
Button("select suzuki") { selectionInt = 2 }
}
.font(Font.body.weight(Font.Weight.bold))
}
}
enum Brand: Int, CaseIterable, Codable, CustomStringConvertible {
case toyota = 0
case mazda = 1
case suzuki = 2
var description: String {
switch self {
case .toyota: return "toyota"
case .mazda: return "mazda"
case .suzuki: return "suzuki"
}
}
}
struct Car {
var brand: Brand
}
Where appropriate, I like defining my enums to be String-based. This is especially useful when the data will be saved to Firebase. When I look at the database, I see the actual values instead of 0s, 1s and 2s that I then have to remember/lookup what they actually stand for.
enum Brand: String, CaseIterable, Codable {
case toyota = "Toyota"
case mazda = "Mazda"
case suzuki = "Suzuki"
}
struct Car {
var brand: Brand
}
struct CarView: View {
#State car: Car
var body: some View {
SomeView(with: $car.brand)
}
}
struct SomeView: some View {
#Binding var brand: Brand
...
}
#State makes it mutable in the original View. #Binding allows another view to make changes and propagate them back to the original view. Going back to the OP's original question, it seems that where you say #Binding should have been #State and you should have a #Binding definition in the SomeView definition. So, you were almost there.
I've been looking through the docs with each beta but haven't seen a way to make a traditional paged ScrollView. I'm not familiar with AppKit so I am wondering if this doesn't exist in SwiftUI because it's primarily a UIKit construct. Anyway, does anyone have an example of this, or can anyone tell me it's definitely impossible so I can stop looking and roll my own?
You can now use a TabView and set the .tabViewStyle to PageTabViewStyle()
TabView {
View1()
View2()
View3()
}
.tabViewStyle(PageTabViewStyle())
As of Beta 3 there is no native SwiftUI API for paging. I've filed feedback and recommend you do the same. They changed the ScrollView API from Beta 2 to Beta 3 and I wouldn't be surprised to see a further update.
It is possible to wrap a UIScrollView in order to provide this functionality now. Unfortunately, you must wrap the UIScrollView in a UIViewController, which is further wrapped in UIViewControllerRepresentable in order to support SwiftUI content.
Gist here
class UIScrollViewViewController: UIViewController {
lazy var scrollView: UIScrollView = {
let v = UIScrollView()
v.isPagingEnabled = true
return v
}()
var hostingController: UIHostingController<AnyView> = UIHostingController(rootView: AnyView(EmptyView()))
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
}
func pinEdges(of viewA: UIView, to viewB: UIView) {
viewA.translatesAutoresizingMaskIntoConstraints = false
viewB.addConstraints([
viewA.leadingAnchor.constraint(equalTo: viewB.leadingAnchor),
viewA.trailingAnchor.constraint(equalTo: viewB.trailingAnchor),
viewA.topAnchor.constraint(equalTo: viewB.topAnchor),
viewA.bottomAnchor.constraint(equalTo: viewB.bottomAnchor),
])
}
}
struct UIScrollViewWrapper<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UIScrollViewViewController {
let vc = UIScrollViewViewController()
vc.hostingController.rootView = AnyView(self.content())
return vc
}
func updateUIViewController(_ viewController: UIScrollViewViewController, context: Context) {
viewController.hostingController.rootView = AnyView(self.content())
}
}
And then to use it:
var body: some View {
GeometryReader { proxy in
UIScrollViewWrapper {
VStack {
ForEach(0..<1000) { _ in
Text("Hello world")
}
}
.frame(width: proxy.size.width) // This ensures the content uses the available width, otherwise it will be pinned to the left
}
}
}
Apple's official tutorial covers this as an example. I find it easy to follow and suitable for my case. I really recommend you check this out and try to understand how to interface with UIKit. Since SwiftUI is so young, not every feature in UIKit would be covered at this moment. Interfacing with UIKit should address most if not all needs.
https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit
Not sure if this helps your question but for the time being while Apple is working on adding a Paging View in SwiftUI I've written a utility library that gives you a SwiftUI feel while using a UIPageViewController under the hood tucked away.
You can use it like this:
Pages {
Text("Page 1")
Text("Page 2")
Text("Page 3")
Text("Page 4")
}
Or if you have a list of models in your application you can use it like this:
struct Car {
var model: String
}
let cars = [Car(model: "Ford"), Car(model: "Ferrari")]
ModelPages(cars) { index, car in
Text("The \(index) car is a \(car.model)")
.padding(50)
.foregroundColor(.white)
.background(Color.blue)
.cornerRadius(10)
}
You can simply track state using .onAppear() to load your next page.
struct YourListView : View {
#ObservedObject var viewModel = YourViewModel()
let numPerPage = 50
var body: some View {
NavigationView {
List(viewModel.items) { item in
NavigationLink(destination: DetailView(item: item)) {
ItemRow(item: item)
.onAppear {
if self.shouldLoadNextPage(currentItem: item) {
self.viewModel.fetchItems(limitPerPage: self.numPerPage)
}
}
}
}
.navigationBarTitle(Text("Items"))
.onAppear {
guard self.viewModel.items.isEmpty else { return }
self.viewModel.fetchItems(limitPerPage: self.numPerPage)
}
}
}
private func shouldLoadNextPage(currentItem item: Item) -> Bool {
let currentIndex = self.viewModel.items.firstIndex(where: { $0.id == item.id } )
let lastIndex = self.viewModel.items.count - 1
let offset = 5 //Load next page when 5 from bottom, adjust to meet needs
return currentIndex == lastIndex - offset
}
}
class YourViewModel: ObservableObject {
#Published private(set) items = [Item]()
// add whatever tracking you need for your paged API like next/previous and count
private(set) var fetching = false
private(set) var next: String?
private(set) var count = 0
func fetchItems(limitPerPage: Int = 30, completion: (([Item]?) -> Void)? = nil) {
// Do your stuff here based on the API rules for paging like determining the URL etc...
if items.count == 0 || items.count < count {
let urlString = next ?? "https://somePagedAPI?limit=/(limitPerPage)"
fetchNextItems(url: urlString, completion: completion)
} else {
completion?(pokemon)
}
}
private func fetchNextItems(url: String, completion: (([Item]?) -> Void)?) {
guard !fetching else { return }
fetching = true
Networking.fetchItems(url: url) { [weak self] (result) in
DispatchQueue.main.async { [weak self] in
self?.fetching = false
switch result {
case .success(let response):
if let count = response.count {
self?.count = count
}
if let newItems = response.results {
self?.items += newItems
}
self?.next = response.next
case .failure(let error):
// Error state tracking not implemented but would go here...
os_log("Error fetching data: %#", error.localizedDescription)
}
}
}
}
}
Modify to fit whatever API you are calling and handle errors based on your app architecture.
Checkout SwiftUIPager. It's a pager built on top of SwiftUI native components:
If you would like to exploit the new PageTabViewStyle of TabView, but you need a vertical paged scroll view, you can make use of effect modifiers like .rotationEffect().
Using this method I wrote a library called VerticalTabView 🔝 that turns a TabView vertical just by changing your existing TabView to VTabView.
You can use such custom modifier:
struct ScrollViewPagingModifier: ViewModifier {
func body(content: Content) -> some View {
content
.onAppear {
UIScrollView.appearance().isPagingEnabled = true
}
.onDisappear {
UIScrollView.appearance().isPagingEnabled = false
}
}
}
extension ScrollView {
func isPagingEnabled() -> some View {
modifier(ScrollViewPagingModifier())
}
}
To simplify Lorenzos answer, you can basically add UIScrollView.appearance().isPagingEnabled = true to your scrollview as below:
VStack{
ScrollView(showsIndicators: false){
VStack(spacing: 0){ // to remove spacing between rows
ForEach(1..<10){ i in
ZStack{
Text(String(i))
Circle()
} .frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
}
}
}.onAppear {
UIScrollView.appearance().isPagingEnabled = true
}
.onDisappear {
UIScrollView.appearance().isPagingEnabled = false
}
}