How to add/remove views from a paged ScrollView (TabView) - swiftui

I am trying to create a paged scrollview
I have used a TabView to create this.
here is my code
struct TT: Identifiable {
let id = UUID()
var v: String
}
struct TTest: View {
#State var currentIndex = 0
#State var data = [
TT(v: "0")
]
var body: some View {
VStack {
SwiftUI.TabView(selection: $currentIndex) {
ForEach(data.indexed(), id: \.1.id) { index, value in
TTestConsomer(data: $data[index]).tag(index)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
Spacer()
HStack {
Image(systemName: "plus")
.resizable()
.frame(width: 50, height: 50)
.onTapGesture(perform: add)
Image(systemName: "minus")
.resizable()
.frame(width: 50, height: 5)
.onTapGesture(perform: delete)
}
Text("\(currentIndex + 1)/\(data.count)")
}
}
func add() {
data.append(TT(v: "\(data.count)") )
}
func delete() {
data.remove(at: currentIndex)
}
}
struct TTestConsomer: View {
#Binding var data: TT
var body: some View {
Text(data.v)
.padding(30)
.border(Color.black)
}
}
// You also need this extension
extension RandomAccessCollection {
func indexed() -> IndexedCollection<Self> {
IndexedCollection(base: self)
}
}
struct IndexedCollection<Base: RandomAccessCollection>: RandomAccessCollection {
typealias Index = Base.Index
typealias Element = (index: Index, element: Base.Element)
let base: Base
var startIndex: Index { base.startIndex }
var endIndex: Index { base.endIndex }
func index(after i: Index) -> Index {
base.index(after: i)
}
func index(before i: Index) -> Index {
base.index(before: i)
}
func index(_ i: Index, offsetBy distance: Int) -> Index {
base.index(i, offsetBy: distance)
}
subscript(position: Index) -> Element {
(index: position, element: base[position])
}
}
When I click on the + button, I can add more tabs.
But when I click on the delete button, My app crashes.
What is the problem here? There does not seem to be anything wrong with the code.

No, I don't think you're doing anything wrong. I submitted an issue to Apple about a related issue on the TabView a few months ago but never heard anything back. The debug comes from the collection view coordinator:
#0 0x00007fff57011ca4 in Coordinator.collectionView(_:cellForItemAt:) ()
It seems like after removing an item, the underlying collection view doesn't get updated. In UIKit, we would probably call .reloadData() to force it to update. In SwiftUI, you could redraw the collection view on the screen to force it to update. Obviously, this affects the UI and isn't a perfect solution.
struct TabViewTest: View {
#State var isLoading: Bool = false
#State var data: [String] = [
"one",
"two"
]
var body: some View {
if !isLoading, !data.isEmpty {
TabView {
ForEach(data, id: \.self) { item in
Text(item)
.tabItem { Text(item) }
}
}
.tabViewStyle(PageTabViewStyle())
.overlay(
HStack {
Text("Add")
.onTapGesture {
add()
}
Text("remove")
.onTapGesture {
remove()
}
}
, alignment: .bottom
)
}
}
func add() {
data.append("three")
print(data)
}
func remove() {
isLoading = true
data.removeFirst()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
isLoading = false
}
print(data)
}
}
struct TabVIewTest_Previews: PreviewProvider {
static var previews: some View {
TabViewTest()
}
}

Related

SwiftUI - #Published array doesn’t update child view

Here's my model:
struct ChatRoom: Identifiable, Equatable {
static func == (lhs: ChatRoom, rhs: ChatRoom) -> Bool {
lhs.id == rhs.id
}
struct LastMessage: Codable {
let isSeen: Bool
var isOfCurrentUser: Bool?
let createdAt: Date
let senderId, text: String
}
let id: String
let userIds: [String]
var lastMessage: LastMessage
let otherUser: User
let currentUserAvatarObject: [String : Any]?
let isTyping: Bool
var blockerIds: [String]
let archiverIds: [String]
let messages: Int
let senderMessages: Int
let receiverMessages: Int
}
I have the ChatRoomsListView view which initializes its ViewModel like so:
struct ChatRoomsListView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
VStack {
HStack {
Button {
} label: {
Image(systemName: "camera")
.scaledToFit()
.foregroundColor(.white)
.frame(width: 30, height: 30)
.padding(6)
.background(
Circle().foregroundColor(.white.opacity(0.15))
)
}
Spacer()
Text("Chats")
.foregroundColor(.white)
.offset(x: -20)
Spacer()
}
ScrollView(.vertical) {
LazyVStack {
ForEach(viewModel.chatRooms) { chatRoom in
ChatRoomView(chatRoom: chatRoom)
}
}
}
}.padding(.vertical, 44)
if viewModel.chatRooms.isEmpty {
Text("It seems you haven’t chatted with anyone yet! That’s ok!")
.foregroundColor(.white)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
}
}
}
private struct ChatRoomView: View {
let chatRoom: ChatRoom
private var text: String {
chatRoom.isTyping ? NSLocalizedString("Typing...", comment: "") : chatRoom.lastMessage.text
}
private var hasUnseenMessage: Bool {
!chatRoom.lastMessage.isSeen && chatRoom.lastMessage.isOfCurrentUser == false
}
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
VStack(spacing: 0) {
HStack {
if hasUnseenMessage {
Circle()
.frame(width: 10, height: 10)
.foregroundColor(.blue)
}
VStack(alignment: .leading) {
Text(chatRoom.otherUser.username)
.foregroundColor(.white)
Text(text)
.foregroundColor(.white)
.lineLimit(1)
}
Spacer()
if chatRoom.lastMessage.isSeen && chatRoom.lastMessage.isOfCurrentUser == true {
Image(systemName: "checkmark.circle")
.foregroundColor(.white)
}
}.padding()
}
}
}
}
And here’s the ViewModel:
extension ChatRoomsListView {
class ViewModel: ObservableObject {
#Published var chatRooms = [ChatRoom]()
// -----------------------------
#Injected private var chatRoomsService: ChatRoomsRepository
#Injected private var currentUserService: CurrentUserRepository
// -----------------------------
init() {
getChatRooms()
subscribeToChatRoomUpdates()
}
private func getChatRooms() {
guard let currentUser = currentUserService.user else { return }
chatRoomsService.getChatRooms(currentUser: currentUser) { [weak self] chatRooms in
self?.chatRooms = chatRooms
}
}
private func subscribeToChatRoomUpdates() {
guard let currentUser = currentUserService.user else { return }
chatRoomsService.subscribeToChatRoomUpdates(currentUser: currentUser) { [weak self] chatRooms in
DispatchQueue.main.async {
for chatRoom in chatRooms {
if let index = self?.chatRooms.firstIndex(where: { $0.id == chatRoom.id }) {
self?.chatRooms[index] = chatRoom
} else {
self?.chatRooms.insert(chatRoom, at: 0)
}
}
self?.chatRooms.removeAll(where: { $0.archiverIds.contains(currentUser.id) })
self?.chatRooms.sort(by: { $0.lastMessage.createdAt > $1.lastMessage.createdAt })
}
}
}
}
}
My problem is that once the getChatRooms is called and changes the chatRooms array for the first time, after that, every time the subscribeToChatRoomUpdates is called doesn't redraw the ChatRoomView child view. On the other hand, ChatRoomsListView gets updated properly.
Why is that happening?
I think your implementation of Equatable - where it only compares id has broken the ability for the ChatView to detect a change to the let chatRoom: ChatRoom, so SwiftUI doesn't call body because the ids are unchanged between the old value and the new value. Try removing Equatable.
By the way, you need to unsubscribe to updates in the object's deinit or you might get a crash. And your use of self? is problematic, look into retain cycles in blocks.

How to Add multi text into the list in SwiftUI?(Data Flow)

I'm trying to build an demo app by swiftUI that get multi text from user and add them to the list, below , there is an image of app every time user press plus button the AddListView show to the user and there user can add multi text to the List.I have a problem to add them to the list by new switUI data Flow I don't know how to pass data.(I comment more information)
Thanks 🙏
here is my code for AddListView:
import SwiftUI
struct AddListView: View {
#State var numberOfTextFiled = 1
#Binding var showAddListView : Bool
var body: some View {
ZStack {
Title(numberOfTextFiled: $numberOfTextFiled)
VStack {
ScrollView {
ForEach(0 ..< numberOfTextFiled, id: \.self) { item in
PreAddTextField()
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView)
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
AddListView(showAddListView: .constant(false))
}
}
struct PreAddTextField: View {
// I made this standalone struct and use #State to every TextField text be independent
// if i use #Binding to pass data all Texfield have the same text value
#State var textInTextField = ""
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
// What should happen here to add Text to List???
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
#Binding var numberOfTextFiled : Int
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
numberOfTextFiled += 1
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
and for DataModel:
import SwiftUI
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
}
var textData = [
Text1(text: "SwiftUI"),
Text1(text: "Data flow?"),
]
and finally:
import SwiftUI
struct ListView: View {
#State var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(textData, id : \.self){ text in
Text(text.text)
}
if showAddListView {
AddListView(showAddListView: $showAddListView)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ListView()
}
}
Because of the multiple-items part of the question, this becomes a lot less trivial. However, using a combination of ObservableObjects and callback functions, definitely doable. Look at the inline comments in the code for explanations about what is going on:
struct Text1 : Identifiable , Hashable{
var id = UUID()
var text : String
}
//Store the items in an ObservableObject instead of just in #State
class AppState : ObservableObject {
#Published var textData : [Text1] = [.init(text: "Item 1"),.init(text: "Item 2")]
}
//This view model stores data about all of the new items that are going to be added
class AddListViewViewModel : ObservableObject {
#Published var textItemsToAdd : [Text1] = [.init(text: "")] //start with one empty item
//save all of the new items -- don't save anything that is empty
func saveToAppState(appState: AppState) {
appState.textData.append(contentsOf: textItemsToAdd.filter { !$0.text.isEmpty })
}
//these Bindings get used for the TextFields -- they're attached to the item IDs
func bindingForId(id: UUID) -> Binding<String> {
.init { () -> String in
self.textItemsToAdd.first(where: { $0.id == id })?.text ?? ""
} set: { (newValue) in
self.textItemsToAdd = self.textItemsToAdd.map {
guard $0.id == id else {
return $0
}
return .init(id: id, text: newValue)
}
}
}
}
struct AddListView: View {
#Binding var showAddListView : Bool
#ObservedObject var appState : AppState
#StateObject private var viewModel = AddListViewViewModel()
var body: some View {
ZStack {
Title(addItem: { viewModel.textItemsToAdd.append(.init(text: "")) })
VStack {
ScrollView {
ForEach(viewModel.textItemsToAdd, id: \.id) { item in //note this is id: \.id and not \.self
PreAddTextField(textInTextField: viewModel.bindingForId(id: item.id))
}
}
}
.padding()
.offset(y: 40)
Buttons(showAddListView: $showAddListView, save: {
viewModel.saveToAppState(appState: appState)
})
}
.frame(width: 300, height: 200)
.background(Color.white)
.shadow(color: Color.black.opacity(0.3), radius: 10, x: 0, y: 10)
}
}
struct PreAddTextField: View {
#Binding var textInTextField : String //this takes a binding to the view model now
var body: some View {
VStack {
TextField("Enter text", text: $textInTextField)
}
}
}
struct Buttons: View {
#Binding var showAddListView : Bool
var save : () -> Void //callback function for what happens when "Add" gets pressed
var body: some View {
VStack {
HStack(spacing:100) {
Button(action: {
showAddListView = false}) {
Text("Cancel")
}
Button(action: {
showAddListView = false
save()
}) {
Text("Add")
}
}
}
.offset(y: 70)
}
}
struct Title: View {
var addItem : () -> Void //callback function for what happens when the plus button is hit
var body: some View {
VStack {
HStack {
Text("Add Text to list")
.font(.title2)
Spacer()
Button(action: {
addItem()
}) {
Image(systemName: "plus")
.font(.title2)
}
}
.padding()
Spacer()
}
}
}
struct ListView: View {
#StateObject var appState = AppState() //store the AppState here
#State private var showAddListView = false
var body: some View {
NavigationView {
VStack {
ZStack {
List(appState.textData, id : \.self){ text in
Text(text.text)
}
if showAddListView {
AddListView(showAddListView: $showAddListView, appState: appState)
.offset(y:-100)
}
}
}
.navigationTitle("List")
.navigationBarItems(trailing:
Button(action: {showAddListView = true}) {
Image(systemName: "plus")
.font(.title2)
}
)
}
}
}

SwiftUI List single selectable item

I'm trying to create a List and allow only one item to be selected at a time. How would I do so in a ForEach loop? I can select multiple items just fine, but the end goal is to have only one checkmark in the selected item in the List. It may not even be the proper way to handle what I'm attempting.
struct ContentView: View {
var body: some View {
NavigationView {
List((1 ..< 4).indices, id: \.self) { index in
CheckmarkView(index: index)
.padding(.all, 3)
}
.listStyle(PlainListStyle())
.navigationBarTitleDisplayMode(.inline)
//.environment(\.editMode, .constant(.active))
}
}
}
struct CheckmarkView: View {
let index: Int
#State var check: Bool = false
var body: some View {
Button(action: {
check.toggle()
}) {
HStack {
Image("Image-\(index)")
.resizable()
.frame(width: 70, height: 70)
.cornerRadius(13.5)
Text("Example-\(index)")
Spacer()
if check {
Image(systemName: "checkmark")
.resizable()
.frame(width: 12, height: 12)
}
}
}
}
}
You'll need something to store all of the states instead of storing it per-checkmark view, because of the requirement to just have one thing checked at a time. I made a little example where the logic is handled in an ObservableObject and passed to the checkmark views through a custom Binding that handles checking/unchecking states:
struct CheckmarkModel {
var id = UUID()
var state = false
}
class StateManager : ObservableObject {
#Published var checkmarks = [CheckmarkModel(), CheckmarkModel(), CheckmarkModel(), CheckmarkModel()]
func singularBinding(forIndex index: Int) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.checkmarks[index].state
} set: { (newValue) in
self.checkmarks = self.checkmarks.enumerated().map { itemIndex, item in
var itemCopy = item
if index == itemIndex {
itemCopy.state = newValue
} else {
//not the same index
if newValue {
itemCopy.state = false
}
}
return itemCopy
}
}
}
}
struct ContentView: View {
#ObservedObject var state = StateManager()
var body: some View {
NavigationView {
List(Array(state.checkmarks.enumerated()), id: \.1.id) { (index, item) in //<-- here
CheckmarkView(index: index + 1, check: state.singularBinding(forIndex: index))
.padding(.all, 3)
}
.listStyle(PlainListStyle())
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct CheckmarkView: View {
let index: Int
#Binding var check: Bool //<-- Here
var body: some View {
Button(action: {
check.toggle()
}) {
HStack {
Image("Image-\(index)")
.resizable()
.frame(width: 70, height: 70)
.cornerRadius(13.5)
Text("Example-\(index)")
Spacer()
if check {
Image(systemName: "checkmark")
.resizable()
.frame(width: 12, height: 12)
}
}
}
}
}
What's happening:
There's a CheckmarkModel that has an ID for each checkbox, and the state of that box
StateManager keeps an array of those models. It also has a custom binding for each index of the array. For the getter, it just returns the state of the model at that index. For the setter, it makes a new copy of the checkbox array. Any time a checkbox is set, it unchecks all of the other boxes. I also kept your original behavior of allowing nothing to be checked
The List now gets an enumeration of the state.checkmarks -- using enumerated lets me keep your previous behavior of being able to pass an index number to the checkbox view
Inside the ForEach, the custom binding from before is created and passed to the subview
In the subview, instead of using #State, #Binding is used (this is what the custom Binding is passed to)
List {
ForEach(0 ..< RemindTimeType.allCases.count) {
index in CheckmarkView(title:getListTitle(index), index: index, markIndex: $markIndex)
.padding(.all, 3)
}.listRowBackground(Color.clear)
}
struct CheckmarkView: View {
let title: String
let index: Int
#Binding var markIndex: Int
var body: some View {
Button(action: {
markIndex = index
}) {
HStack {
Text(title)
.foregroundColor(Color.white)
.font(.custom(FontEnum.Regular.fontName, size: 14))
Spacer()
if index == markIndex {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(Color(hex: 0xe6c27c))
}
}
}
}
}
We can benefit from binding collections of Swift 5.5.
import SwiftUI
struct CheckmarkModel: Identifiable, Hashable {
var id = UUID()
var state = false
}
class StateManager : ObservableObject {
#Published var checkmarks = [CheckmarkModel(), CheckmarkModel(), CheckmarkModel(), CheckmarkModel()]
}
struct SingleSelectionList<Content: View>: View {
#Binding var items: [CheckmarkModel]
#Binding var selectedItem: CheckmarkModel?
var rowContent: (CheckmarkModel) -> Content
#State var previouslySelectedItemNdx: Int?
var body: some View {
List(Array($items.enumerated()), id: \.1.id) { (ndx, $item) in
rowContent(item)
.modifier(CheckmarkModifier(checked: item.id == self.selectedItem?.id))
.contentShape(Rectangle())
.onTapGesture {
if let prevIndex = previouslySelectedItemNdx {
items[prevIndex].state = false
}
self.selectedItem = item
item.state = true
previouslySelectedItemNdx = ndx
}
}
}
}
struct CheckmarkModifier: ViewModifier {
var checked: Bool = false
func body(content: Content) -> some View {
Group {
if checked {
ZStack(alignment: .trailing) {
content
Image(systemName: "checkmark")
.resizable()
.frame(width: 20, height: 20)
.foregroundColor(.green)
.shadow(radius: 1)
}
} else {
content
}
}
}
}
struct ContentView: View {
#ObservedObject var state = StateManager()
#State private var selectedItem: CheckmarkModel?
var body: some View {
VStack {
Text("Selected Item: \(selectedItem?.id.description ?? "Select one")")
Divider()
SingleSelectionList(items: $state.checkmarks, selectedItem: $selectedItem) { item in
HStack {
Text(item.id.description + " " + item.state.description)
Spacer()
}
}
}
}
}
A bit simplified version
struct ContentView: View {
#ObservedObject var state = StateManager()
#State private var selection: CheckmarkModel.ID?
var body: some View {
List {
ForEach($state.checkmarks) { $item in
SelectionCell(item: $item, selectedItem: $selection)
.onTapGesture {
if let ndx = state.checkmarks.firstIndex(where: { $0.id == selection}) {
state.checkmarks[ndx].state = false
}
selection = item.id
item.state = true
}
}
}
.listStyle(.plain)
}
}
struct SelectionCell: View {
#Binding var item: CheckmarkModel
#Binding var selectedItem: CheckmarkModel.ID?
var body: some View {
HStack {
Text(item.id.description + " " + item.state.description)
Spacer()
if item.id == selectedItem {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
}
A version that uses internal List's selected mark and selection:
import SwiftUI
struct CheckmarkModel: Identifiable, Hashable {
var name: String
var state: Bool = false
var id = UUID()
}
class StateManager : ObservableObject {
#Published var checkmarks = [CheckmarkModel(name: "Name1"), CheckmarkModel(name: "Name2"), CheckmarkModel(name: "Name3"), CheckmarkModel(name: "Name4")]
}
struct ContentView: View {
#ObservedObject var state = StateManager()
#State private var selection: CheckmarkModel.ID?
#State private var selectedItems = [CheckmarkModel]()
var body: some View {
VStack {
Text("Items")
List($state.checkmarks, selection: $selection) { $item in
Text(item.name + " " + item.state.description)
}
.onChange(of: selection) { s in
for index in state.checkmarks.indices {
if state.checkmarks[index].state == true {
state.checkmarks[index].state = false
}
}
selectedItems = []
if let ndx = state.checkmarks.firstIndex(where: { $0.id == selection}) {
state.checkmarks[ndx].state = true
selectedItems = [state.checkmarks[ndx]]
print(selectedItems)
}
}
.environment(\.editMode, .constant(.active))
Divider()
List(selectedItems) {
Text($0.name + " " + $0.state.description)
}
}
Text("\(selectedItems.count) selections")
}
}

How to create a custom delete button without using the slide to delete that comes with swiftUI I am not using list, just using a foreach loop

Since, the onDelete and onMove are features of List/form I cannot use them when I have custom interfaces without them. I have used a VStack inside a ForEach. I am quite new to swiftUI and unsure on how I can implement custom code for onDelete and onMove.
Here's my code:
struct Trying: View {
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
NavigationView {
VStack (spacing: 10) {
ForEach(numbers, id: \.self) { number in
VStack {
Text("\(number)")
}
.frame(width: 50, height: 50)
.background(Color.red)
}.onDelete(perform: removeRows)
}
.navigationTitle("Trying")
.navigationBarItems(trailing: EditButton())
}
}
func removeRows(at offsets: IndexSet) {
numbers.remove(atOffsets: offsets)
}
}
The way it works right now:
Here is a simple demo of possible approach to implement custom delete (of course with move it would be more complicated due to drag/drop, but idea is the same). Tested with Xcode 12 / iOS 14.
struct DemoCustomDelete: View {
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
NavigationView {
VStack (spacing: 10) {
ForEach(numbers, id: \.self) { number in
VStack {
Text("\(number)")
}
.frame(width: 50, height: 50)
.background(Color.red)
.overlay(
DeleteButton(number: number, numbers: $numbers, onDelete: removeRows)
, alignment: .topTrailing)
}.onDelete(perform: removeRows)
}
.navigationTitle("Trying")
.navigationBarItems(trailing: EditButton())
}
}
func removeRows(at offsets: IndexSet) {
withAnimation {
numbers.remove(atOffsets: offsets)
}
}
}
struct DeleteButton: View {
#Environment(\.editMode) var editMode
let number: Int
#Binding var numbers: [Int]
let onDelete: (IndexSet) -> ()
var body: some View {
VStack {
if self.editMode?.wrappedValue == .active {
Button(action: {
if let index = numbers.firstIndex(of: number) {
self.onDelete(IndexSet(integer: index))
}
}) {
Image(systemName: "minus.circle")
}
.offset(x: 10, y: -10)
}
}
}
}
Based on #Asperi's answer, I just generalized it to accept any Equatable sequence.
struct DeleteButton<T>: View where T: Equatable {
#Environment(\.editMode) var editMode
let number: T
#Binding var numbers: [T]
let onDelete: (IndexSet) -> ()
var body: some View {
VStack {
if self.editMode?.wrappedValue == .active {
Button(action: {
if let index = numbers.firstIndex(of: number) {
self.onDelete(IndexSet(integer: index))
}
}) {
Image(systemName: "minus.circle")
}
.offset(x: 10, y: -10)
}
}
}
}
I recently had the need to delete a row and I couldn't use a LIST. Instead I had a scroll view... But I was able to implement the edit to simulate the same onDelete behavior as if it was a list. Initially I couldn't get my code to work. It wasn't until I closely examined my implementation and experimented that I stumbled on why mine worked. I'm coding for an iPad so my NavigationView uses,
.navigationViewStyle(StackNavigationViewStyle())
Once I added this to the struct's NavigationView, when you click on the EditButton it activates editMode?.wrappedValue to .active / .inactive
Below is my implementation for the code sample above...
struct Trying: View {
#State var num: Int = 0
#Environment(\.editMode) var editMode
#State private var numbers = [0,1,2,3,4,5,6,7,8,9]
var body: some View {
NavigationView {
VStack {
ForEach(numbers, id: \.self) { number in
HStack {
if editMode?.wrappedValue == .active {
Button(action: { num = number
removeRows(numbr: num)
}, label: {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
})
} // END IF editMode?wrappedValue == .active
Text("\(number)")
.frame(width: 50, height: 50)
.background(Color.red)
}
}
// .onDelete(perform: removeRows)
}
.navigationTitle("Trying")
.navigationBarItems(trailing: EditButton())
}
// FOR SOME REASON THIS ALLOWS THE EditButton() to activate editMode without a LIST being present.
.navigationViewStyle(StackNavigationViewStyle())
}
func removeRows(numbr: Int) {
print("removing \(numbr)")
}
}
It looks like:

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.