I wrote a ContinuedFractionView to draw continued fraction structure on my (macos) app.
It works fine when my CF list is relatively short. However, when the list gets a bit longer ( 7, 8, ... ), UI window nearly stops (unresponsive for minutes).
Why? and how could I improve my view?
import SwiftUI
struct ContinuedFractionView: View {
let continuedFraction: [Int]
var body: some View {
HStack(alignment: .top) {
if continuedFraction.count > 1 {
VStack {
Text("")
Text(String(continuedFraction[0]))
}
VStack {
Text("")
Text("+")
}
VStack {
Text("1")
Divider()
.background(Color.black)
ContinuedFractionView(continuedFraction: Array(continuedFraction[1...]))
}
} else {
Text(String(continuedFraction[0]))
}
}
}
}
struct ContinuedFractionView_Previews: PreviewProvider {
static var previews: some View {
ContinuedFractionView(continuedFraction: [1,2,3,4,5,6,7])
}
}
Does it have to be done recursively? If not, it should be pretty straightforward to draw this:
struct ContentView: View {
let fracs = [1,2,3,4,5]
var body: some View {
List {
Section("Mine") {
NonRecursive(fracs: fracs)
}
Section("Yours") {
ContinuedFractionView(continuedFraction: fracs)
}
}
.listStyle(.plain)
}
}
struct NonRecursive: View {
let fracs: [Int]
var body: some View {
VStack(alignment: .leading) {
ForEach(Array(fracs.dropLast().enumerated()), id: \.1) { (index, frac) in
HStack(alignment: .top) {
HStack {
Spacer(minLength: 0)
VStack(alignment: .trailing) {
Text("")
Text(frac, format: .number) + Text(" +")
}
}
.frame(width: CGFloat(index + 1) * 25)
VStack {
Text("1")
Divider()
.background(Color.black)
if let last = fracs.last, index == fracs.count - 2 {
Text(last, format: .number)
}
}
}
}
}
}
}
Related
I want to customize the navigation bar, but the refresh control is covered by the custom navigation bar after hiding the navigation. How can I adjust the position of the refresh control?
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
HomeNavigationBarView {
List {
NavigationLink {
EmptyView()
} label: {
Text("Hello, world!")
}
}
.listStyle(.insetGrouped)
.refreshable {
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Here is my custom navbar
struct HomeNavigationBarView<Content: View>: View {
let content: () -> Content
var body: some View {
ZStack(alignment: .top) {
content()
HStack {
Image("logo")
.resizable()
.frame(width: 44, height: 44)
}
.frame(height: insetTop(), alignment: .bottom)
.frame(maxWidth: .infinity)
.background(.red.opacity(0.1))
}
.navigationBarHidden(true)
}
}
private func insetTop() -> CGFloat {
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
let window = windowScene?.windows.first
return window?.safeAreaInsets.top ?? 0 // 20或47
}
Padding should be there in List .padding(.top, insetTop()) to fix position of refresh control.
struct ContentView: View {
var body: some View {
NavigationView {
HomeNavigationBarView {
List {
NavigationLink {
EmptyView()
} label: {
Text("Hello, world!")
}
}
.padding(.top, insetTop())
.refreshable {
}
}
}
}
}
The way of adding safe area in iOS15+ perfectly solves this problem
List {
}
.safeAreaInset(edge: .top, spacing: 0) {
Rectangle()
.fill(.clear)
.frame(height: insetTop() + 44)
}
How can I fit the GeometryReader to the middle ExpandingDrawer contents?
(Copy & Paste-able):
import SwiftUI
struct ExpandingDrawerButton: View {
#Binding var isExpanded: Bool
var body: some View {
Button(action: { withAnimation { isExpanded.toggle() } }) {
Text(isExpanded ? "Close" : "Open")
}
}
}
struct ExpandingDrawer<Content: View>: View {
#Binding var isExpanded: Bool
var content: () -> Content
var body: some View {
content()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: nil, maxHeight: contentHeight)
.allowsHitTesting(isExpanded)
.clipped()
.transition(.slide)
}
private var contentHeight: CGFloat? {
isExpanded ? nil : CGFloat(0)
}
}
struct DrawerTestView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
struct ContentView: View {
#State var isExpanded = false
var body: some View {
GeometryReader { geo in
VStack(spacing: 0) {
top
.frame(height: geo.size.height * 1/4)
middle
bottom
.frame(height: geo.size.width)
}
}
}
var top: some View {
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) {
Rectangle()
.foregroundColor(.blue.opacity(0.2))
ExpandingDrawerButton(isExpanded: $isExpanded)
.padding()
}
}
var middle: some View {
ExpandingDrawer(isExpanded: $isExpanded) {
middleContent
}
}
var middleContent: some View {
GeometryReader { geo in
VStack {
ForEach(0..<10) { _ in
Button(action: {}) { Text("Random shit") }
}
Text("Don't know how tall...")
Text("Height can change...")
Text("But does need to fit snug (no extra space)")
}
}
}
var bottom: some View {
ZStack {
Rectangle()
.foregroundColor(.red)
.aspectRatio(1, contentMode: .fit)
VStack {
Text("Needs to be a square...")
Text("Okay if pushed below edge of screen...")
}
}
}
}
}
I've tried various combinations of .fixedSize() and .aspectRatio() but I'm struggling...
To avoid GeometryReader changing our views, we can put it in an .overlay().
Example:
struct ContentView: View {
class Storage {
var geo: GeometryProxy! {
didSet {
print(geo.size)
}
}
}
let storage = Storage()
/* ... */
}
var middleContent: some View {
VStack {
ForEach(0..<10) { _ in
Button(action: {}) { Text("Random stuff") }
}
Text("Don't know how tall...")
Text("Height can change...")
Text("But does need to fit snug (no extra space)")
}
.overlay(
GeometryReader { geo in
Rectangle()
.fill(Color.clear)
let _ = storage.geo = geo
}
)
}
You can now use storage.geo to access the GeometryProxy of this view.
How to have both a button in navigation bar and a list with sections?
Here is a code with .navigationBarItems:
struct AllMatchesView: View {
#Environment(\.managedObjectContext) var moc
#State var events = EventData()
var body: some View {
NavigationView {
List{
ForEach(events.sections) { section in
Section(header: Text(section.title)) {
ForEach(section.matches) { match in
Text("Row")
}
}
.navigationBarTitle("Title")
.navigationBarItems(trailing:
NavigationLink(destination: AddMatchView().environment(\.managedObjectContext, self.moc)) {
Image(systemName: "plus")
.resizable()
.frame(width: 22, height: 22)
.padding(.horizontal)
}
.padding(.leading)
)
}
}.onAppear(){
self.events = EventData()
}
}
}
}
Without .navigationBarItems:
struct AllMatchesView: View {
#Environment(\.managedObjectContext) var moc
#State var events = EventData()
var body: some View {
NavigationView {
List{
ForEach(events.sections) { section in
Section(header: Text(section.title)) {
ForEach(section.matches) { match in
Text("Row")
}
}
.navigationBarTitle("Title")
}
}.onAppear(){
self.events = EventData()
}
}
}
}
Result with .navigationBarItems:
Result without .navigationBarItems:
Just move those modifiers out of dynamic content, otherwise you try to include duplicated bar items for every section, that seems makes SwiftUI engine crazy.
var body: some View {
NavigationView {
List{
ForEach(events.sections) { section in
Section(header: Text(section.title)) {
ForEach(section.matches) { match in
Text("Row")
}
}
}
}
.navigationBarTitle("Title")
.navigationBarItems(trailing:
NavigationLink(destination: AddMatchView().environment(\.managedObjectContext, self.moc)) {
Image(systemName: "plus")
.resizable()
.frame(width: 22, height: 22)
.padding(.horizontal)
}
.padding(.leading)
)
.onAppear(){
self.events = EventData()
}
}
}
I am trying to implement a sticky footer in a List View in SwiftUI
It doesn't seem to operate the same as the header per say. This is an example of a sticky header implementation
List {
ForEach(0..<10) { index in
Section(header: Text("Hello")) {
ForEach(0..<2) { index2 in
VStack {
Rectangle().frame(height: 600).backgroundColor(Color.blue)
}
}
}.listRowInsets(EdgeInsets())
}
}
This above gives a sticky header situation. Although, once I change Section(header: ... to Section(footer:... it doesn't seem to be sticky anymore, it's simply places at the end of the row.
A more explicit reference
List {
ForEach(0..<10) { index in
Section(footer: Text("Hello")) {
ForEach(0..<2) { index2 in
VStack {
Rectangle().frame(height: 600).backgroundColor(Color.blue)
}
}
}.listRowInsets(EdgeInsets())
}
}
Does anyone have any solutions for this?
With the latest on SwiftUI (2) we now have access to a few more API's
For starters we can use a LazyVStack with a ScrollView to give us pretty good performance, we can then use the pinnedViews API to specify in an array which supplementary view we want to pin or make sticky. We can then use the Section view which wraps our content and specify either a footer or header.
** This code is working as of Xcode beta 2 **
As for using this in a List I'm not too sure, will be interesting to see the performance with List vs Lazy...
struct ContentView: View {
var body: some View {
ScrollView {
LazyVStack(spacing: 10, pinnedViews: [.sectionFooters]) {
ForEach(0..<20, id: \.self) { index in
Section(footer: FooterView(index: index)) {
ForEach(0..<6) { _ in
Rectangle().fill(Color.red).frame(height: 100).id(UUID())
}
}
}
}
}
}
}
struct FooterView: View {
let index: Int
var body: some View {
VStack {
Text("Footer \(index)").padding(5)
}.background(RoundedRectangle(cornerRadius: 4.0).foregroundColor(.green))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
You can use overlay on the List:
struct ContentView: View {
#State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
VStack {
List {
ForEach(0..<20, id: \.self) { _ in
Section {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
}
}
.listStyle(InsetGroupedListStyle())
.overlay(
VStack {
Spacer()
Text("Updated at: 5:26 AM")
.font(.footnote)
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
}
)
}
.tabItem {
Label("First", systemImage: "alarm")
}
Text("Content 2")
.tabItem {
Label("Second", systemImage: "calendar")
}
}
}
}
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()
}
}
}