SwiftUI how to Reorder in Landscape and Portrait - swiftui

we use a custom gridView to display all Products in a grid.
the problem is... we need to reorder if the Device is in Landscape or Portait.
How can we do that?
ModularGridStyle(columns: 2, rows: 3)
columns and rows needs to be different if device is in landscape.
here is the View
var body: some View {
Grid(self.products) { product in
Text("\(product.name)")
}
.gridStyle(
ModularGridStyle(columns: 2, rows: 3)
)
}

I would use verticalSizeClass.
#Environment(\.verticalSizeClass) private var verticalSizeClass: UserInterfaceSizeClass?
var body: some View {
Grid(self.products) { Text(verbatim: "\($0.name)") }
.gridStyle(
ModularGridStyle(
columns: self.verticalSizeClass == .compact ? 3 : 2,
rows: self.verticalSizeClass == .compact ? 2 : 3
)
)
}
}

I personally prefer using a GeometryReader to setup different views for portrait/landscape. Of course, it's a bit redundant, but usually you have other properties that change as well:
var body: some View {
GeometryReader() { g in
if g.size.width < g.size.height {
// view in portrait mode
Grid(self.products) { product in
Text("\(product.name)")
}
.gridStyle(
ModularGridStyle(columns: 2, rows: 3)
)
} else {
// view in landscape mode
Grid(self.products) { product in
Text("\(product.name)")
}
.gridStyle(
ModularGridStyle(columns: 3, rows: 2)
)
}
}
}

Here is possible approach:
private var deviceOrientationPublisher = NotificationCenter.default.publisher(for:
UIDevice.orientationDidChangeNotification)
#State var dimention: (Int, Int) = (2, 3)
var body: some View {
Grid(self.products) { product in
Text("\(product.name)")
}
.gridStyle(
ModularGridStyle(columns: dimention.0, rows: dimention.1)
)
.onReceive(deviceOrientationPublisher) { _ in
self.dimention = UIDevice.current.orientation.isLandscape ? (3, 2) : (2, 3)
}
}

I looked for a more simple & elegant solution -
and found this code in - hackingwithswift
// Our custom view modifier to track rotation and
// call our action
struct DeviceRotationViewModifier: ViewModifier {
let action: (UIDeviceOrientation) -> Void
func body(content: Content) -> some View {
content
.onAppear()
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
action(UIDevice.current.orientation)
}
}
}
// A View wrapper to make the modifier easier to use
extension View {
func onRotate(perform action: #escaping (UIDeviceOrientation) -> Void) -> some View {
self.modifier(DeviceRotationViewModifier(action: action))
}
}
// An example view to demonstrate the solution
struct ContentView: View {
#State private var orientation = UIDeviceOrientation.unknown
var body: some View {
Group {
if orientation.isPortrait {
Text("Portrait")
} else if orientation.isLandscape {
Text("Landscape")
} else if orientation.isFlat {
Text("Flat")
} else {
Text("Unknown")
}
}
.onRotate { newOrientation in
orientation = newOrientation
}
}
}

Related

What's the best way to achieve parameterized "on tap"/"on click" behavior for a list row?

So let's say I have a list component in SwiftUI:
struct MyListView: View {
var body: some View {
List(...) { rec in
Row(rec)
}
}
}
Now let's say I want to make this reusable, and I want the "caller" of this view to determine what happens when I tap on each row view. What would be the correct way to insert that behavior?
Here is some other Buttons in ListView example that you can run and play with it yourself
import SwiftUI
struct TestTableView: View {
#State private var item: MyItem?
var body: some View {
NavigationView {
List {
// Cell as Button that display Sheet
ForEach(1...3, id:\.self) { i in
Button(action: { item = MyItem(number: i) }) {
TestTableViewCell(number: i)
}
}
// Cell as NavigationLink
ForEach(4...6, id:\.self) { i in
NavigationLink(destination: TestTableViewCell(number: i)) {
TestTableViewCell(number: i)
}
}
// If you want a button inside cell which doesn't trigger the whole cell when being touched
HStack {
TestTableViewCell(number: 7)
Spacer()
Button(action: { item = MyItem(number: 7) }) {
Text("Button").foregroundColor(.accentColor)
}.buttonStyle(PlainButtonStyle())
}
}
}.sheet(item: $item) { myItem in
TestTableViewCell(number: myItem.number)
}
}
struct MyItem: Identifiable {
var number: Int
var id: Int { number }
}
}
struct TestTableViewCell: View {
var number: Int
var body: some View {
Text("View Number \(number)")
}
}
Make it like Button and takes an action param that is a closure.
From my understanding you're looking for a reusable generic List view with tap on delete functionality. If I'm guessing right my approach then would be like this:
struct MyArray: Identifiable {
let id = UUID()
var title = ""
}
struct ContentView: View {
#State private var myArray = [
MyArray(title: "One"),
MyArray(title: "Two"),
MyArray(title: "Three"),
MyArray(title: "Four"),
MyArray(title: "Five"),
]
var body: some View {
MyListView(array: myArray) { item in
Text(item.title) // row view
} onDelete: { item in
myArray.removeAll(where: {$0.id == item.id}) // delete func
}
}
}
struct MyListView<Items, Label>: View
where Items: RandomAccessCollection, Items.Element: Identifiable, Label: View {
var array: Items
var row: (Items.Element) -> Label
var onDelete: (Items.Element) -> ()
var body : some View {
List(array) { item in
Button {
onDelete(item)
} label: {
row(item)
}
}
}
}

SwiftUICharts are not redrawn when given new data

I am adding the possibility to swipe in order to update a barchart. What I want to show is statistics for different station. To view different station I want the user to be able to swipe between the stations. I can see that the swiping works and each time I swipe I get the correct data from my controller. The problem is that my view is not redrawn properly.
I found this guide, but cannot make it work.
Say I swipe right from station 0 with data [100, 100, 100] to station 2, the retrieved data from my controller is [0.0, 100.0, 0.0]. The view I have still is for [100, 100, 100]`.
The station number is correctly updated, so I suspect it needs some state somehow.
Here is the code:
import SwiftUI
import SwiftUICharts
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName()).tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}
struct BarCharts: View {
var data: [Double]
var title: String
init(data: [Double], disciplineName: String) {
self.data = data
title = disciplineName
print(data)
}
var body: some View {
VStack {
BarChartView(data: ChartData(points: self.data), title: self.title, style: Styles.barChartStyleOrangeLight, form: CGSize(width: 300, height: 400))
}
}
}
class ViewModel: ObservableObject {
#Published var station = 1
let controller = DetailedViewController()
var isPreview = false
func getData(kLatestRounds: Int, station: Int) -> [Double] {
if isPreview {
return [100.0, 100.0, 100.0]
} else {
let data = controller.getResults(kLatestRounds: kLatestRounds, station: station, fileName: userDataFile)
return data
}
}
func getName() -> String {
controller.getDiscipline().name
}
func getNumberOfStations() -> Int {
controller.getDiscipline().getNumberOfStations()
}
func getStation() -> Int {
station
}
func incrementStation() {
station = (station + 1) % getNumberOfStations()
}
func decrementStation() {
station -= 1
if station < 0 {
station = getNumberOfStations() - 1
}
}
}
The data is printed inside the constructor each time I swipe. Shouldn't that mean it should be updated?
I don’t use SwiftUICharts so I can’t test it, but the least you can try is manually set the id to the view
struct DetailedResultsView: View {
#ObservedObject var viewModel: ViewModel = .init()
#State private var tabIndex: Int = 0
#State private var startPos: CGPoint = .zero
#State private var isSwiping = true
var body: some View {
VStack {
Text("Station \(viewModel.getStation() + 1)")
TabView(selection: $tabIndex) {
BarCharts(data: viewModel.getData(kLatestRounds: 10, station: viewModel.getStation()), disciplineName: viewModel.getName())
.id(viewmodel.station) // here. If it doesn’t work, you can set it to the whole TabView
.tabItem { Group {
Image(systemName: "chart.bar")
Text("Last 10 Sessions")
}}.tag(0)
}
}.gesture(DragGesture()
.onChanged { gesture in
if self.isSwiping {
self.startPos = gesture.location
self.isSwiping.toggle()
}
}
.onEnded { gesture in
if gesture.location.x - startPos.x > 10 {
viewModel.decrementStation()
}
if gesture.location.x - startPos.x < -10 {
viewModel.incrementStation()
}
}
)
}
}

SwiftUI List not updating after Data change. (Needs switching the view)

I'm trying to change the SwiftUI list to update after tapping the checkbox button in my list.
When I tap on a list row checkbox button, I call a function to set the immediate rows as checked which ID is less then the selected one. I could modify the ArrayList as selected = 0 to selected = 1. But as my list is Published var it should emit the change to my list view. but it doesn't.
Here's what I've done:
// ViewModel
import Foundation
import Combine
class BillingViewModel: ObservableObject {
#Published var invoiceList = [InvoiceModel](){
didSet {
self.didChange.send(true)
}
}
#Published var shouldShow = true
let didChange = PassthroughSubject<Bool, Never>()
init() {
setValues()
}
func setValues() {
for i in 0...10 {
invoiceList.append(InvoiceModel(ispID: 100+i, selected: 0, invoiceNo: "Invoice No: \(100+i)"))
}
}
func getCombinedBalance(ispID: Int) {
DispatchQueue.main.async {
if let row = self.invoiceList.firstIndex(where: {$0.ispID == ispID}) {
self.changeSelection(row: row)
}
}
}
func changeSelection(row: Int) {
if invoiceList[row].selected == 0 {
let selectedRows = invoiceList.map({ $0.ispID ?? 0 <= invoiceList[row].ispID ?? 0 })
print(selectedRows)
for index in 0..<invoiceList.count {
if selectedRows[index] {
invoiceList[index].selected = 1
} else {
invoiceList[index].selected = 0
}
}
} else {
let selectedRows = invoiceList.map({ $0.ispID ?? 0 <= invoiceList[row].ispID ?? 0 })
print(selectedRows)
for index in 0..<invoiceList.count {
if selectedRows[index] {
invoiceList[index].selected = 1
} else {
invoiceList[index].selected = 0
}
}
}
}
}
// List View
import SwiftUI
struct ContentView: View {
#StateObject var billingViewModel = BillingViewModel()
#State var shouldShow = true
var body: some View {
NavigationView {
ZStack {
VStack {
List {
ForEach(billingViewModel.invoiceList) { invoice in
NavigationLink(
destination: InvoiceDetailsView(billingViewModel: billingViewModel)) {
invoiceRowView(billingViewModel: billingViewModel, invoice: invoice)
}
}
}
}
}.navigationBarTitle(Text("Invoice List"))
}
}
}
// Invoice Row View
import SwiftUI
struct invoiceRowView: View {
#StateObject var billingViewModel: BillingViewModel
#State var invoice: InvoiceModel
var body: some View {
VStack(alignment: .leading) {
HStack {
Button(action: {
if invoice.selected == 0 {
print(invoice)
billingViewModel.getCombinedBalance(ispID: invoice.ispID ?? 0)
} else {
print(invoice)
billingViewModel.getCombinedBalance(ispID: invoice.ispID ?? 0)
}
}, label: {
Image(systemName: invoice.selected == 0 ? "checkmark.circle" : "checkmark.circle.fill")
.resizable()
.frame(width: 32, height: 32, alignment: .center)
}).buttonStyle(PlainButtonStyle())
Text(invoice.invoiceNo ?? "Hello, World!").padding()
Spacer()
}
}
}
}
// Data Model
import Foundation
struct InvoiceModel: Codable, Identifiable {
var id = UUID()
var ispID: Int?
var selected: Int?
var invoiceNo: String?
}
You need to use binding instead of externally injected state (which is set just to copy of value), i.e.
struct invoiceRowView: View {
#StateObject var billingViewModel: BillingViewModel
#Binding var invoice: InvoiceModel
// .. other code
and thus inject binding in parent view
ForEach(billingViewModel.invoiceList.indices, id: \.self) { index in
NavigationLink(
destination: InvoiceDetailsView(billingViewModel: billingViewModel)) {
invoiceRowView(billingViewModel: billingViewModel, invoice: $billingViewModel.invoiceList[index])
}
}

SwiftUI ScrollView not getting updated?

Goal
Get data to display in a scrollView
Expected Result
Actual Result
Alternative
use List, but it is not flexible (can't remove separators, can't have multiple columns)
Code
struct Object: Identifiable {
var id: String
}
struct Test: View {
#State var array = [Object]()
var body: some View {
// return VStack { // uncomment this to see that it works perfectly fine
return ScrollView(.vertical) {
ForEach(array) { o in
Text(o.id)
}
}
.onAppear(perform: {
self.array = [Object(id: "1"),Object(id: "2"),Object(id: "3"),Object(id: "4"),Object(id: "5")]
})
}
}
A not so hacky way to get around this problem is to enclose the ScrollView in an IF statement that checks if the array is empty
if !self.array.isEmpty{
ScrollView(.vertical) {
ForEach(array) { o in
Text(o.id)
}
}
}
I've found that it works (Xcode 11.2) as expected if state initialised with some value, not empty array. In this case updating works correctly and initial state have no effect.
struct TestScrollViewOnAppear: View {
#State var array = [Object(id: "1")]
var body: some View {
ScrollView(.vertical) {
ForEach(array) { o in
Text(o.id)
}
}
.onAppear(perform: {
self.array = [Object(id: "1"),Object(id: "2"),Object(id: "3"),Object(id: "4"),Object(id: "5")]
})
}
}
One hacky workaround I've found is to add an "invisible" Rectangle inside the scrollView, with the width set to a value greater than the width of the data in the scrollView
struct Object: Identifiable {
var id: String
}
struct ContentView: View {
#State var array = [Object]()
var body: some View {
GeometryReader { geometry in
ScrollView(.vertical) {
Rectangle()
.frame(width: geometry.size.width, height: 0.01)
ForEach(array) { o in
Text(o.id)
}
}
}
.onAppear(perform: {
self.array = [Object(id: "1"),Object(id: "2"),Object(id: "3"),Object(id: "4"),Object(id: "5")]
})
}
}
The expected behavior occurs on Xcode 11.1 but doesn't on Xcode 11.2.1
I added a frame modifier to the ScrollView which make the ScrollView appear
struct Object: Identifiable {
var id: String
}
struct ContentView: View {
#State var array = [Object]()
var body: some View {
ScrollView(.vertical) {
ForEach(array) { o in
Text(o.id)
}
}
.frame(height: 40)
.onAppear(perform: {
self.array = [Object(id: "1"),Object(id: "2"),Object(id: "3"),Object(id: "4"),Object(id: "5")]
})
}
}
Borrowing from paulaysabellemedina, I came up with a little helper view that handles things the way I expected.
struct ScrollingCollection<Element: Identifiable, Content: View>: View {
let items: [Element]
let axis: Axis.Set
let content: (Element) -> Content
var body: some View {
Group {
if !self.items.isEmpty {
ScrollView(axis) {
if axis == .horizontal {
HStack {
ForEach(self.items) { item in
self.content(item)
}
}
}
else {
VStack {
ForEach(self.items) { item in
self.content(item)
}
}
}
}
}
else {
EmptyView()
}
}
}
}

SwiftUI: Switch between List and grid/Collection View

Goal: A button that switches between List and Grid/Collection View.
For this, I am using the great WaterfallGrid:
https://github.com/paololeonardi/WaterfallGrid
I haven't managed able to make it work. I am using state, and if statement, as code bellow:
import SwiftUI
import WaterfallGrid
struct Fruit: Identifiable {
let id = UUID()
let name: String
let image: Image
}
struct ExampleView: View {
#State private var fruits = [
Fruit(name: "Apple", image: Image("apple")),
Fruit(name: "Banana", image: Image("banana")),
Fruit(name: "Grapes", image: Image("grapes")),
Fruit(name: "Peach", image: Image("peach"))]
#State private var showgrid = true
var body: some View {
NavigationView {
if showgrid == .true {
return
WaterfallGrid(fruits) { fruit in
HStack {
fruit.image.resizable().frame(width: 30, height: 30)
Text(fruit.name)
}
}
}
else {
return
List(fruits) { fruit in
HStack {
fruit.image.resizable().frame(width: 30, height: 30)
Text(fruit.name)
}
}
}
.navigationBarTitle("Fruits")
.navigationBarItems(trailing:
Button(action: { self.showmaterialrmenu.toggle() }) {
Image(systemName: "rectangle.on.rectangle.angled")
})
}
}
}
struct ExampleView_Previews: PreviewProvider {
static var previews: some View {
ExampleView()
}
}
Really appreciate any help!
Cheers
Hello it's not a good way to put all you views in one place I created one project that need to be having a list and grid mode so here's my implementation:
private enum HomeMode {
case list, grid
func icon() -> String {
switch self {
case .list: return "rectangle.3.offgrid.fill"
case .grid: return "rectangle.grid.1x2"
}
}
}
#State private var homeMode = HomeMode.list
private var swapHomeButton: some View {
Button(action: {
self.homeMode = self.homeMode == .grid ? .list : .grid
}) {
HStack {
Image(systemName: self.homeMode.icon()).imageScale(.medium)
}.frame(width: 30, height: 30)
}
}
and here's my main view:
var body: some View {
let view = Group {
if homeMode == .list {
homeAsList
} else {
homeAsGrid
}
}
.navigationBarItems(trailing:
HStack {
swapHomeButton
settingButton
}
).sheet(isPresented: $isSettingPresented,
content: { SettingsForm() })
return navigationView(content: AnyView(view))
}
HomeList:
private var homeAsList: some View {
Group {
if selectedMenu.menu == .genres {
GenresList(headerView: AnyView(segmentedView))
} else {
MoviesHomeList(menu: $selectedMenu.menu,
pageListener: selectedMenu.pageListener,
headerView: AnyView(segmentedView))
}
}
}
private var homeAsGrid: some View {
MoviesHomeGrid()
}
that's the answer If you are asking about how to arrange the views but if you are asking how to implement the WaterfallGrid hit me back so I can check it and see if we can solve this