default value for #ViewBuilder closure? - swiftui

Can we give #ViewBuilder closure parameter a default value?
This question arose when I was doing some experiments:
import SwiftUI
import PlaygroundSupport
// MyView
struct MyView<S:View, T:View>: View {
let groove: S
let bar : T
let p : CGFloat = 10 // padding
// ⭐️ no default values
init(#ViewBuilder groove: () -> S, #ViewBuilder bar: () -> T) {
self.groove = groove()
self.bar = bar()
}
var body: some View {
ZStack {
groove
bar.padding(p)
}.frame(height: 80)
}
}
// content view
struct ContentView: View {
var body: some View {
// using MyView
MyView(groove: {
Gradient.down(.gray, .white) // ⭐️ my custom LinearGradient extension
}, bar: {
Gradient.right(.purple, .yellow, .pink) // ⭐️ my custom extension
})
}
}
PlaygroundPage.current.setLiveView(ContentView())
(⭐️ my custom Gradient extension is mentioned here.)
and the result was quite good:
When I tried to go a little further and gave those #ViewBuilder closures default values, everything went bad:
import SwiftUI
import PlaygroundSupport
struct MyView<S:View, T:View>: View {
let groove: S
let bar : T
let p : CGFloat = 10 // padding
// ⭐️ try to give #ViewBuilder closures default values
init(
#ViewBuilder
groove: () -> S = { Gradient.down(.gray, .white) } as! () -> S,
#ViewBuilder
bar: () -> T = { Gradient.right(.purple, .yellow, .pink) } as! () -> T
) {
self.groove = groove()
self.bar = bar()
}
var body: some View {
ZStack {
groove
bar.padding(p)
}.frame(height: 80)
}
}
struct ContentView: View {
var body: some View {
VStack {
// ❌ can't infer `T`
MyView(groove: {
Gradient.down(.gray, .white)
})
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
Type parameter T can not be inferred from the code above.
Any ideas?
----[edited]----
I've made some efforts to my question, and here is what I've got so far:
import SwiftUI
import PlaygroundSupport
// default values for #ViewBuilder parameters
#ViewBuilder var defaultGroove: some View {
Gradient.down(.gray, .white)
}
#ViewBuilder var defaultBar: some View {
Gradient.right(.purple, .yellow, .pink)
}
// MyView
struct MyView<S:View, T:View>: View {
let groove: S
let bar : T
// ⭐️ try to give #ViewBuilder parameters default value
init(
#ViewBuilder groove: () -> S = { defaultGroove as! S },
#ViewBuilder bar : () -> T = { defaultBar as! T }
) {
self.groove = groove()
self.bar = bar()
}
var body: some View {
ZStack {
groove
bar.padding(10)
}.frame(height: 80)
}
}
// Content View
struct ContentView: View {
var body: some View {
VStack {
// `bar` omitted
MyView<LinearGradient, LinearGradient>(groove: {
Gradient.bottomRight(.white, .gray, .white)
})
// `groove` omitted
MyView<LinearGradient, Color>(bar: {
Color.pink
})
// both omitted
MyView<LinearGradient, LinearGradient>()
// ❌ can't infer `S`
// ⭐️ it would be perfect if `S` can be inferred.
// MyView(bar: {
// Gradient.right(.purple, .white)
// })
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
and the result is:
It would be perfect if both parameters could be inferred automatically.
----[ edited again ]----
According to #Asperi's previous advise, I've made my third attempt:
import SwiftUI
import PlaygroundSupport
// default values for #ViewBuilder parameters
#ViewBuilder var defaultGroove: some View {
Gradient.down(.gray, .white)
}
#ViewBuilder var defaultBar: some View {
Gradient.right(.purple, .yellow, .pink)
}
// MyView
struct MyView<S:View, T:View>: View {
let groove: S
let bar : T
// default value for #ViewBuilder parameters
init(
#ViewBuilder groove: () -> S = { defaultGroove as! S },
#ViewBuilder bar: () -> T = { defaultBar as! T }
) {
self.groove = groove()
self.bar = bar()
}
var body: some View {
ZStack {
groove
bar.padding(10)
}.frame(height: 80)
}
}
// ⭐️ conditional extensions for convenience inits
extension MyView where T == LinearGradient {
/// MyView(groove:)
init(#ViewBuilder groove: () -> S){
self.init(
groove: groove,
bar : { defaultBar as! T }
)
}
}
extension MyView where S == LinearGradient {
/// MyView(bar:)
init(#ViewBuilder bar: () -> T){
self.init(
groove: { defaultGroove as! S },
bar : bar
)
}
}
extension MyView where S == LinearGradient, T == LinearGradient {
/// MyView()
init(){
self.init(
groove: { defaultGroove as! S },
bar : { defaultBar as! T }
)
}
}
// Content View
struct ContentView: View {
var body: some View {
VStack {
// ⭐️ `S`, `T` are both inferred in the following cases
MyView(groove: {
Gradient.bottomRight(.white, .yellow, .green)
}, bar: {
Color(white: 0.8)
.shadow(color: .black, radius: 3, x: 3, y: 3)
.shadow(color: .white, radius: 3, x: -3, y: -3)
})
// `bar` omitted
MyView(groove: {
Gradient.right(.red, .purple)
})
// `groove` omitted
MyView(bar: {
Gradient.right(.purple, .white)
.shadow(color: .black, radius: 3, x: 0, y: 2)
})
// `groove`, `bar` both omitted
MyView()
}
}
}
PlaygroundPage.current.setLiveView(ContentView())
and the result is:
I've implemented all the needed extensions for those convenience intializers, it works but if we could find a way to avoid those extensions in the first place, it would be perfect.
Is it possible?

Here is a demo of possible approach (you can substitute your Gradient type) - you need extension with default init for specialised types.
Tested as worked with Xcode 12 / iOS 14
extension MyView where S == Text, T == Button<Text> {
init() {
self.init(groove: { Text("Hello") },
bar: { Button("World", action: { }) })
}
}
and then you can just use MyView() which generates your default groove and bar.

Related

How we can adding a search bar with side bar icon to the navigation view?

I want to add a search bar to the navigation bar, but I do not know how to use search bar with sidebar icon in the same HStack. I put example screenshot with ContentView code. Any help would be appreciated.
Screenshot:
ContentView:
struct ContentView: View {
#State private var isShowing = false
var body: some View {
ZStack {
if isShowing {
SideMenuView(isShowing: $isShowing)
}
TabView {
NavigationView {
HomeView()
.navigationBarItems(leading: Button(action: {
withAnimation(.spring()) {
isShowing.toggle()
}
} , label: {
Image(systemName: "list.bullet")
}))
}
.tabItem {
Image(systemName: "1.circle")
Text("Page 1")
}
NavigationView {
HomeTwoView()
.navigationBarItems(leading: Button(action: {
withAnimation(.spring()) {
isShowing.toggle()
}
} , label: {
Image(systemName: "list.bullet")
}))
}
.tabItem {
Image(systemName: "2.circle")
Text("Page 2")
}
}
.edgesIgnoringSafeArea(.bottom)
//.cornerRadius(isShowing ? 20 : 0) //<< disabled due to strange effect
.offset(x: isShowing ? 300 : 0, y: isShowing ? 44: 0)
.scaleEffect(isShowing ? 0.8 : 1)
}.onAppear {
isShowing=false
}
}
}
As I mentioned in comments this is not possible in SwiftUI (2.0) yet. What you can do is integrating with UIKit.
Integrate with UIKit
class UIKitSearchBar: NSObject, ObservableObject {
#Published var text: String = ""
let searchController = UISearchController(searchResultsController: nil)
override init() {
super.init()
self.searchController.obscuresBackgroundDuringPresentation = false
self.searchController.definesPresentationContext = true
self.searchController.searchResultsUpdater = self
}
}
extension UIKitSearchBar: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
// Publish search bar text changes.
if let searchBarText = searchController.searchBar.text {
self.text = searchBarText
}
}
}
struct SearchBarModifier: ViewModifier {
let searchBar: UIKitSearchBar
func body(content: Content) -> some View {
content
.overlay(
ViewControllerResolver { viewController in
viewController.navigationItem.searchController = self.searchBar.searchController
}
.frame(width: 0, height: 0)
)
}
}
extension View {
func add(_ searchBar: UIKitSearchBar) -> some View {
return self.modifier(SearchBarModifier(searchBar: searchBar))
}
}
final class ViewControllerResolver: UIViewControllerRepresentable {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
}
func makeUIViewController(context: Context) -> ParentResolverViewController {
ParentResolverViewController(onResolve: onResolve)
}
func updateUIViewController(_ uiViewController: ParentResolverViewController, context: Context) { }
}
class ParentResolverViewController: UIViewController {
let onResolve: (UIViewController) -> Void
init(onResolve: #escaping (UIViewController) -> Void) {
self.onResolve = onResolve
super.init(nibName: nil, bundle: nil)
}
#available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
if let parent = parent {
onResolve(parent)
}
}
}
Usage
struct Example: View {
#StateObject var searchBar = UIKitSearchBar()
var body: some View {
NavigationView {
Text("Example")
.add(searchBar)
.navigationTitle("Example")
}
}
}
In my own project I am using computed property to filter stuff, it can be helpful for you too. Here is my code:
var filteredExams: [Exam] {
examModel.exams.filter({ searchBar.text.isEmpty || $0.examName.localizedStandardContains(searchBar.text)})
}
Screenshot

How do I make a preferenceKey accept a View in SwiftUI?

I'm trying to build a custom NavigationView, and I'm struggling with how to implement a custom ".navigationBarItems(leading: /* insert views /, trailing: / insert Views */)". I assume I have to use a preferenceKey, but I don't know how to make it accept views.
My top menu looks something like this:
import SwiftUI
struct TopMenu<Left: View, Right: View>: View {
let leading: Left
let trailing: Right
init(#ViewBuilder left: #escaping () -> Left, #ViewBuilder right: #escaping () -> Right) {
self.leading = left()
self.trailing = right()
}
var body: some View {
VStack(spacing: 0) {
HStack {
leading
Spacer()
trailing
}.frame(height: 30, alignment: .center)
Spacer()
}
.padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
}
}
struct TopMenu_Previews: PreviewProvider {
static var previews: some View {
TopMenu(left: { }, right: { })
}
}
And this is my attempt at creating a preferenceKey to update it with, where I've obviously missed something very basic:
struct TopMenuItemsLeading: PreferenceKey {
static var defaultValue:View
static func reduce(value: inout View, nextValue: () -> View) {
value = nextValue()
}
}
struct TopMenuItemsTrailing: PreferenceKey {
static var defaultValue:View
static func reduce(value: inout View, nextValue: () -> View) {
value = nextValue()
}
}
extension View {
func topMenuItems(leading: View, trailing: View) -> some View {
self.preference(key: TopMenuItemsLeading.self, value: leading)
self.preference(key: TopMenuItemsTrailing.self, value: trailing)
}
}
Ok, so there was some great partial answers in here, but none that actually achieved what I asked, which was to pass a view up the view-hierarchy using a preferenceKey. Essentially what the .navigationBarItems method is doing, but with my own custom view.
I found a solution however, so here goes (and apologies if I missed any obvious short-cuts. This IS my first time using preferenceKeys for anything):
import SwiftUI
struct TopMenu: View {
#State private var show:Bool = false
var body: some View {
VStack {
TopMenuView {
Button("Change", action: { show.toggle() })
Text("Hello world!")
.topMenuItems(leading: Image(systemName: show ? "xmark.circle" : "pencil"))
.topMenuItems(trailing: Image(systemName: show ? "pencil" : "xmark.circle"))
}
}
}
}
struct TopMenu_Previews: PreviewProvider {
static var previews: some View {
TopMenu()
}
}
/*
To emulate .navigationBarItems(leading: View, trailing: View), I need four things:
1) EquatableViewContainer - Because preferenceKeys need to be equatable to be able to update when a change occurred
2) PreferenceKeys - That use the EquatableViewContainer for both leading and trailing views
3) ViewExtenstions - That allow us to set the preferenceKeys individually or one at a time
4) TopMenu view - That we can set somewhere higher in the view hierarchy.
*/
// First, create an EquatableViewContainer we can use as preferenceKey data
struct EquatableViewContainer: Equatable {
let id = UUID().uuidString
let view:AnyView
static func == (lhs: EquatableViewContainer, rhs: EquatableViewContainer) -> Bool {
return lhs.id == rhs.id
}
}
// Second, define preferenceKeys that uses the Equatable view container
struct TopMenuItemsLeading: PreferenceKey {
static var defaultValue: EquatableViewContainer = EquatableViewContainer(view: AnyView(EmptyView()) )
static func reduce(value: inout EquatableViewContainer, nextValue: () -> EquatableViewContainer) {
value = nextValue()
}
}
struct TopMenuItemsTrailing: PreferenceKey {
static var defaultValue: EquatableViewContainer = EquatableViewContainer(view: AnyView(EmptyView()) )
static func reduce(value: inout EquatableViewContainer, nextValue: () -> EquatableViewContainer) {
value = nextValue()
}
}
// Third, create view-extensions for each of the ways to modify the TopMenu
extension View {
// Change only leading view
func topMenuItems<LView: View>(leading: LView) -> some View {
self
.preference(key: TopMenuItemsLeading.self, value: EquatableViewContainer(view: AnyView(leading)))
}
// Change only trailing view
func topMenuItems<RView: View>(trailing: RView) -> some View {
self
.preference(key: TopMenuItemsTrailing.self, value: EquatableViewContainer(view: AnyView(trailing)))
}
// Change both leading and trailing views
func topMenuItems<LView: View, TView: View>(leading: LView, trailing: TView) -> some View {
self
.preference(key: TopMenuItemsLeading.self, value: EquatableViewContainer(view: AnyView(leading)))
}
}
// Fourth, create the view for the TopMenu
struct TopMenuView<Content: View>: View {
// Content to put into the menu
let content: Content
#State private var leading:EquatableViewContainer = EquatableViewContainer(view: AnyView(EmptyView()))
#State private var trailing:EquatableViewContainer = EquatableViewContainer(view: AnyView(EmptyView()))
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content()
}
var body: some View {
VStack(spacing: 0) {
ZStack {
HStack {
leading.view
Spacer()
trailing.view
}
Text("TopMenu").fontWeight(.black)
}
.padding(EdgeInsets(top: 0, leading: 2, bottom: 5, trailing: 2))
.background(Color.gray.edgesIgnoringSafeArea(.top))
content
Spacer()
}
.onPreferenceChange(TopMenuItemsLeading.self, perform: { value in
leading = value
})
.onPreferenceChange(TopMenuItemsTrailing.self, perform: { value in
trailing = value
})
}
}
`````
The possible approach is to use AnyView, like
struct TopMenuItemsLeading: PreferenceKey {
static var defaultValue: AnyView = AnyView(EmptyView())
static func reduce(value: inout AnyView, nextValue: () -> AnyView) {
value = nextValue()
}
}
struct TopMenuItemsTrailing: PreferenceKey {
static var defaultValue: AnyView = AnyView(EmptyView())
static func reduce(value: inout AnyView, nextValue: () -> AnyView) {
value = nextValue()
}
}
extension View {
func topMenuItems<LView: View, TView: View>(leading: LView, trailing: TView) -> some View {
self
.preference(key: TopMenuItemsLeading.self, value: AnyView(leading))
.preference(key: TopMenuItemsTrailing.self, value: AnyView(trailing))
}
}
You could declare the initialiser of TopView to take Views like this:
struct TopMenu<Left: View, Right: View>: View {
let leading: Left
let trailing: Right
init(left: Left,
right: Right) {
self.leading = left
self.trailing = right
}
//etc.
And then declare the modifier similarly to how navigationBarItems modifiers are defined:
extension View {
func topMenuItems<L, T>(leading: L, trailing: T) -> some View where L : View, T : View {
VStack(alignment: .center, spacing: 0) {
TopMenu(left: leading, right: trailing)
self
}
}
func topMenuItems<L>(leading: L) -> some View where L : View {
VStack(alignment: .center, spacing: 0) {
TopMenu(left: leading, right: EmptyView())
self
}
}
func topMenuItems<T>(trailing: T) -> some View where T : View {
VStack(alignment: .center, spacing: 0) {
TopMenu(left: EmptyView(), right: trailing)
self
}
}
}

How to loop over viewbuilder content subviews in SwiftUI

So I’m trying to create a view that takes viewBuilder content, loops over the views of the content and add dividers between each view and the other
struct BoxWithDividerView<Content: View>: View {
let content: () -> Content
init(#ViewBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
// here
}
.background(Color.black)
.cornerRadius(14)
}
}
so where I wrote “here” I want to loop over the views of the content, if that makes sense. I’ll write a code that doesn’t work but that explains what I’m trying to achieve:
ForEach(content.subviews) { view in
view
Divider()
}
How to do that?
I just answered on another similar question, link here. Any improvements to this will be made for the linked answer, so check there first.
GitHub link of this (but more advanced) in a Swift Package here
However, here is the answer with the same TupleView extension, but different view code.
Usage:
struct ContentView: View {
var body: some View {
BoxWithDividerView {
Text("Something 1")
Text("Something 2")
Text("Something 3")
Image(systemName: "circle") // Different view types work!
}
}
}
Your BoxWithDividerView:
struct BoxWithDividerView: View {
let content: [AnyView]
init<Views>(#ViewBuilder content: #escaping () -> TupleView<Views>) {
self.content = content().getViews
}
var body: some View {
VStack(alignment: .center, spacing: 0) {
ForEach(content.indices, id: \.self) { index in
if index != 0 {
Divider()
}
content[index]
}
}
// .background(Color.black)
.cornerRadius(14)
}
}
And finally the main thing, the TupleView extension:
extension TupleView {
var getViews: [AnyView] {
makeArray(from: value)
}
private struct GenericView {
let body: Any
var anyView: AnyView? {
AnyView(_fromValue: body)
}
}
private func makeArray<Tuple>(from tuple: Tuple) -> [AnyView] {
func convert(child: Mirror.Child) -> AnyView? {
withUnsafeBytes(of: child.value) { ptr -> AnyView? in
let binded = ptr.bindMemory(to: GenericView.self)
return binded.first?.anyView
}
}
let tupleMirror = Mirror(reflecting: tuple)
return tupleMirror.children.compactMap(convert)
}
}
Result:
So I ended up doing this
#_functionBuilder
struct UIViewFunctionBuilder {
static func buildBlock<V: View>(_ view: V) -> some View {
return view
}
static func buildBlock<A: View, B: View>(
_ viewA: A,
_ viewB: B
) -> some View {
return TupleView((viewA, Divider(), viewB))
}
}
Then I used my function builder like this
struct BoxWithDividerView<Content: View>: View {
let content: () -> Content
init(#UIViewFunctionBuilder content: #escaping () -> Content) {
self.content = content
}
var body: some View {
VStack(spacing: 0.0) {
content()
}
.background(Color(UIColor.AdUp.carbonGrey))
.cornerRadius(14)
}
}
But the problem is this only works for up to 2 expression views. I’m gonna post a separate question for how to be able to pass it an array

SwiftUI chat app: the woes of reversed List and Context Menu

I am building a chat app in SwiftUI. To show messages in a chat, I need a reversed list (the one that shows most recent entries at the bottom and auto-scrolls to the bottom). I made a reversed list by flipping both the list and each of its entries (the standard way of doing it).
Now I want to add Context Menu to the messages. But after the long press, the menu shows messages flipped. Which I suppose makes sense since it plucks a flipped message out of the list.
Any thoughts on how to get this to work?
import SwiftUI
struct TestView: View {
var arr = ["1aaaaa","2bbbbb", "3ccccc", "4aaaaa","5bbbbb", "6ccccc", "7aaaaa","8bbbbb", "9ccccc", "10aaaaa","11bbbbb", "12ccccc"]
var body: some View {
List {
ForEach(arr.reversed(), id: \.self) { item in
VStack {
Text(item)
.height(100)
.scaleEffect(x: 1, y: -1, anchor: .center)
}
.contextMenu {
Button(action: { }) {
Text("Reply")
}
}
}
}
.scaleEffect(x: 1, y: -1, anchor: .center)
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
The issue with flipping is that you need to flip the context menu and SwiftUI does not give this much control.
The better way to handle this is to get access to embedded UITableView(on which you will have more control) and you need not add additional hacks.
Here is the demo code:
import SwiftUI
import UIKit
struct TestView: View {
#State var arr = ["1aaaaa","2bbbbb", "3ccccc", "4aaaaa","5bbbbb", "6ccccc", "7aaaaa","8bbbbb", "9ccccc", "10aaaaa","11bbbbb", "12ccccc"]
#State var tableView: UITableView? {
didSet {
self.tableView?.adaptToChatView()
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.tableView?.scrollToBottom(animated: true)
}
}
}
var body: some View {
NavigationView {
List {
UIKitView { (tableView) in
DispatchQueue.main.async {
self.tableView = tableView
}
}
ForEach(arr, id: \.self) { item in
Text(item).contextMenu {
Button(action: {
// change country setting
}) {
Text("Choose Country")
Image(systemName: "globe")
}
Button(action: {
// enable geolocation
}) {
Text("Detect Location")
Image(systemName: "location.circle")
}
}
}
}
.navigationBarTitle(Text("Chat View"), displayMode: .inline)
.navigationBarItems(trailing:
Button("add chat") {
self.arr.append("new Message: \(self.arr.count)")
self.tableView?.adaptToChatView()
DispatchQueue.main.async {
self.tableView?.scrollToBottom(animated: true)
}
})
}
}
}
extension UITableView {
func adaptToChatView() {
let offset = self.contentSize.height - self.visibleSize.height
if offset < self.contentOffset.y {
self.tableHeaderView = UIView.init(frame: CGRect.init(x: 0, y: 0, width: self.contentSize.width, height: self.contentOffset.y - offset))
}
}
}
extension UIScrollView {
func scrollToBottom(animated:Bool) {
let offset = self.contentSize.height - self.visibleSize.height
if offset > self.contentOffset.y {
self.setContentOffset(CGPoint(x: 0, y: offset), animated: animated)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
final class UIKitView : UIViewRepresentable {
let callback: (UITableView) -> Void //return TableView in CallBack
init(leafViewCB: #escaping ((UITableView) -> Void)) {
callback = leafViewCB
}
func makeUIView(context: Context) -> UIView {
let view = UIView.init(frame: CGRect(x: CGFloat.leastNormalMagnitude,
y: CGFloat.leastNormalMagnitude,
width: CGFloat.leastNormalMagnitude,
height: CGFloat.leastNormalMagnitude))
view.backgroundColor = .clear
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
if let tableView = uiView.next(UITableView.self) {
callback(tableView) //return tableview if find
}
}
}
extension UIResponder {
func next<T: UIResponder>(_ type: T.Type) -> T? {
return next as? T ?? next?.next(type)
}
}
You can create a custom modal for reply and show it with long press on every element of the list without showing contextMenu.
#State var showYourCustomReplyModal = false
#GestureState var isDetectingLongPress = false
var longPress: some Gesture {
LongPressGesture(minimumDuration: 0.5)
.updating($isDetectingLongPress) { currentstate, gestureState,
transaction in
gestureState = currentstate
}
.onEnded { finished in
self.showYourCustomReplyModal = true
}
}
Apply it like:
ForEach(arr, id: \.self) { item in
VStack {
Text(item)
.height(100)
.scaleEffect(x: 1, y: -1, anchor: .center)
}.gesture(self.longPress)
}
As of iOS 14, SwiftUI has ScrollViewReader which can be used to position the scrolling. GeometryReader along with minHeight and Spacer() can make a VStack that uses the full screen while displaying messages starting at the bottom. Items are read from and appended to an array in the usual first-in first-out order.
SwiftUI example:
struct ContentView: View {
#State var items: [Item] = []
#State var text: String = ""
#State var targetItem: Item?
var body: some View {
VStack {
ScrollViewReader { scrollView in
ChatStyleScrollView() {
ForEach(items) { item in
ItemView(item: item)
.id(item.id)
}
}
.onChange(of: targetItem) { item in
if let item = item {
withAnimation(.default) {
scrollView.scrollTo(item.id)
}
}
}
TextEntryView(items: $items, text: $text, targetItem: $targetItem)
}
}
}
}
//MARK: - Item Model with unique identifier
struct Item: Codable, Hashable, Identifiable {
var id: UUID
var text: String
}
//MARK: - ScrollView that pushes text to the bottom of the display
struct ChatStyleScrollView<Content: View>: View {
let content: Content
init(#ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
GeometryReader { proxy in
ScrollView(.vertical, showsIndicators: false) {
VStack {
Spacer()
content
}
.frame(minHeight: proxy.size.height)
}
}
}
}
//MARK: - A single item and its layout
struct ItemView: View {
var item: Item
var body: some View {
HStack {
Text(item.text)
.frame(height: 100)
.contextMenu {
Button(action: { }) {
Text("Reply")
}
}
Spacer()
}
}
}
//MARK: - TextField and Send button used to input new items
struct TextEntryView: View {
#Binding var items: [Item]
#Binding var text: String
#Binding var targetItem: Item?
var body: some View {
HStack {
TextField("Item", text: $text)
.frame(height: 44)
Button(action: send) { Text("Send") }
}
.padding(.horizontal)
}
func send() {
guard !text.isEmpty else { return }
let item = Item(id: UUID(), text: text)
items.append(item)
text = ""
targetItem = item
}
}
If someone is searching for a solution in UIKit: instead of the cell, you should use the contentView or a subview of the contentView as a paramterer for the UITargetedPreview. Like this:
extension CustomScreen: UITableViewDelegate {
func tableView(_ tableView: UITableView,
contextMenuConfigurationForRowAt indexPath: IndexPath,
point: CGPoint) -> UIContextMenuConfiguration? {
UIContextMenuConfiguration(identifier: indexPath as NSCopying,
previewProvider: nil) { _ in
// ...
return UIMenu(title: "", children: [/* actions */])
}
}
func tableView(
_ tableView: UITableView,
previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
getTargetedPreview(for: configuration.identifier as? IndexPath)
}
func tableView(
_ tableView: UITableView,
previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
getTargetedPreview(for: configuration.identifier as? IndexPath)
}
}
extension CustomScreen {
private func getTargetedPreview(for indexPath: IndexPath?) -> UITargetedPreview? {
guard let indexPath = indexPath,
let cell = tableView.cellForRow(at: indexPath) as? CustomTableViewCell else { return nil }
return UITargetedPreview(view: cell.contentView,
parameters: UIPreviewParameters().then { $0.backgroundColor = .clear })
}
}
If I understood it correctly, why don't you order your array in the for each loop or prior. Then you do not have to use any scaleEffect at all. Later if you get your message object, you probably have a Date assinged to it, so you can order it by the date. In your case above you could use:
ForEach(arr.reverse(), id: \.self) { item in
...
}
Which will print 12ccccc as first message at the top, and 1aaaaa as last message.

Detect touch outside Button in SwiftUI

I have a reset button that asks for confirmation first. I would like to set isSure to false is the user touches outside the component.
Can I do this from the Button component?
Here is my button:
struct ResetButton: View {
var onConfirmPress: () -> Void;
#State private var isSure: Bool = false;
var body: some View {
Button(action: {
if (self.isSure) {
self.onConfirmPress();
self.isSure.toggle();
} else {
self.isSure.toggle();
}
}) {
Text(self.isSure ? "Are you sure?" : "Reset")
}
}
}
here is one way to do it:
struct ContentView: View {
var onConfirmPress: () -> Void
#State private var isSure: Bool = false
var body: some View {
GeometryReader { geometry in
ZStack {
// a transparent rectangle under everything
Rectangle()
.frame(width: geometry.size.width, height: geometry.size.height)
.opacity(0.001) // <--- important
.layoutPriority(-1)
.onTapGesture {
self.isSure = false
print("---> onTapGesture self.isSure : \(self.isSure)")
}
Button(action: {
if (self.isSure) {
self.onConfirmPress()
}
self.isSure.toggle()
}) {
Text(self.isSure ? "Are you sure?" : "Reset").padding(10).border(Color.black)
}
}
}
}
}
Basically, we have some view, and we want a tap on its background to do something - meaning, we want to add a huge background that registers a tap. Note that .background is only offered the size of the main view, but can always set an explicit different size! If you know your size that's great, otherwise UIScreen could work...
This is hacky but seems to work!
extension View {
#ViewBuilder
private func onTapBackgroundContent(enabled: Bool, _ action: #escaping () -> Void) -> some View {
if enabled {
Color.clear
.frame(width: UIScreen.main.bounds.width * 2, height: UIScreen.main.bounds.height * 2)
.contentShape(Rectangle())
.onTapGesture(perform: action)
}
}
func onTapBackground(enabled: Bool, _ action: #escaping () -> Void) -> some View {
background(
onTapBackgroundContent(enabled: enabled, action)
)
}
}
Usage:
SomeView()
.onTapBackground(enabled: isShowingAlert) {
isShowingAlert = false
}
This can be easily changed to take a binding:
func onTapBackground(set value: Binding<Bool>) -> some View {
background(
onTapBackgroundContent(enabled: value.wrappedValue) { value.wrappedValue = false }
)
}
// later...
SomeView()
.onTapBackground(set: $isShowingAlert)