How to complement UIScrollView in navigationView? - swiftui

I'm currently developing an application using SwiftUI and trying to refresh data using pull action.
When I implement the function in List it works, but if I use that in NavigationView the function doesn't work...
// ---OK↓---
RefreshScrollViewTest(refreshControl: self.$refreshControl)
// ---NG↓---
NavigationView{
RefreshScrollViewTest(refreshControl: self.$refreshControl)
}
Is there any way to use the function in NavigationView?
Here are the codes:
import SwiftUI
struct NavigationRefreshTest: View {
#State var refreshControl = UIRefreshControl()
var body: some View {
// NavigationView{
RefreshScrollViewTest(refreshControl: self.$refreshControl)
// }
}
}
struct RefreshListTest:View {
#Binding var refreshControl:UIRefreshControl
var body: some View{
List{
Text("test1")
Text("test2")
Text("test3")
}
.onAppear{
NotificationCenter.default.addObserver(forName: NSNotification.Name("Update"), object: nil, queue: .main){ (_) in
DispatchQueue.main.asyncAfter(deadline: .now() + 1){
print("update...")
}
}
}
}
}
struct RefreshScrollViewTest:UIViewRepresentable {
func makeCoordinator() ->Coodinator {
return RefreshScrollViewTest.Coodinator()
}
#Binding var refreshControl:UIRefreshControl
func makeUIView(context: Context) -> UIScrollView {
let view = UIScrollView()
self.refreshControl.attributedTitle = NSAttributedString(string: "Loding")
self.refreshControl.addTarget(context.coordinator, action: #selector(context.coordinator.update), for: .valueChanged)
view.showsVerticalScrollIndicator = false
view.refreshControl = self.refreshControl
view.contentSize = CGSize(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
let child = UIHostingController(rootView: RefreshListTest(refreshControl: self.$refreshControl))
child.view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width,
height: UIScreen.main.bounds.height)
view.addSubview(child.view)
return view
}
func updateUIView(_ uiView: UIScrollView, context: Context) {
}
class Coodinator:NSObject{
#objc func update(){
NotificationCenter.default.post(name: NSNotification.Name("Update"), object: nil)
}
}
}
Xcode: Version 12.3
iOS: 14.0

I haven't solved your problem, but you should use this library SwiftUIRefresh

Related

SiwftUI: update model in UIViewRepresentable Coordinator when View updates?

So I am using a WKWebView within UIViewRepresentable so I can show a web view in my SwiftUI view.
For a while I could not figure out why my SwiftUI view would not update when the Coordinator would set #Publsihed properties that affect the SwiftUI view.
In the process I finally understood better how UIViewRepresentable works and realized what the problem was.
This is the UIViewRepresentable:
struct SwiftUIWebView : UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
....
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
// This made my SwiftUI view update properly when the web view would report loading progress etc..
context.coordinator.viewModel = viewModel
}
}
The SwiftUI view would pass in the viewModel, then makeCoordinator would be called (only the first time at init...), then the Coordinator would be returned with that viewModel.
However, subsequently when a new viewModel was passed in on updates and not on coordinator init, the coordinator would just keep the old viewModel and things would stop working.
So I added this in the updateUIView... call, which did fix the problem:
context.coordinator.viewModel = viewModel
Question:
Is there a way to pass in the viewModel to the Coordinator during the func makeUIView(context: Context) -> WKWebView { ... } so that if a new viewModel is passed in to SwiftUIWebView the coordinator would also get the change automatically instead of me having to add:
context.coordinator.viewModel = viewModel
in updateUIView...?
EDIT:
Here is the entire code. The root content view:
struct ContentView: View {
#State var showTestModal = false
#State var redrawTest = false
var body: some View {
NavigationView {
VStack {
Button(action: {
showTestModal.toggle()
}) {
Text("Show modal")
}
if redrawTest {
Text("REDRAW")
}
}
}
.fullScreenCover(isPresented: $showTestModal) {
WebContentViewTest(redraw: $redrawTest)
}
}
}
And what the Content view presents:
struct SwiftUIProgressBar: View {
#Binding var progress: Double
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .leading) {
Rectangle()
.foregroundColor(Color.gray)
.opacity(0.3)
.frame(width: geometry.size.width, height: geometry.size.height)
Rectangle()
.foregroundColor(Color.blue)
.frame(width: geometry.size.width * CGFloat((self.progress)),
height: geometry.size.height)
.animation(.linear(duration: 0.5))
}
}
}
}
struct SwiftUIWebView : UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
func makeCoordinator() -> Coordinator {
Coordinator(self, viewModel: viewModel)
}
let webView = WKWebView()
func makeUIView(context: Context) -> WKWebView {
print("SwiftUIWebView MAKE")
if let url = URL(string: viewModel.link) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
func updateUIView(_ uiView: WKWebView, context: Context) {
//add your code here...
}
}
class Coordinator: NSObject {
private var viewModel: WebViewModel
var parent: SwiftUIWebView
private var estimatedProgressObserver: NSKeyValueObservation?
init(_ parent: SwiftUIWebView, viewModel: WebViewModel) {
print("Coordinator init")
self.parent = parent
self.viewModel = viewModel
super.init()
estimatedProgressObserver = self.parent.webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in
print(Float(webView.estimatedProgress))
guard let weakSelf = self else{return}
print("in progress observer: model is: \(Unmanaged.passUnretained(weakSelf.parent.viewModel).toOpaque())")
weakSelf.parent.viewModel.progress = webView.estimatedProgress
}
}
deinit {
estimatedProgressObserver = nil
}
}
class WebViewModel: ObservableObject {
#Published var progress: Double = 0.0
#Published var link : String
init (progress: Double, link : String) {
self.progress = progress
self.link = link
print("model init: \(Unmanaged.passUnretained(self).toOpaque())")
}
}
struct WebViewContainer: View {
#ObservedObject var model: WebViewModel
var body: some View {
ZStack {
SwiftUIWebView(viewModel: model)
VStack {
if model.progress >= 0.0 && model.progress < 1.0 {
SwiftUIProgressBar(progress: .constant(model.progress))
.frame(height: 15.0)
.foregroundColor(.accentColor)
}
Spacer()
}
}
}
}
struct WebContentViewTest : View {
#Binding var redraw:Bool
var body: some View {
let _ = print("WebContentViewTest body")
NavigationView {
ZStack(alignment: .topLeading) {
if redraw {
WebViewContainer(model: WebViewModel(progress: 0.0, link: "https://www.google.com"))
}
VStack {
Button(action: {
redraw.toggle()
}) {
Text("redraw")
}
Spacer()
}
}
.navigationBarTitle("Test Modal", displayMode: .inline)
}
}
}
If you run this you will see that while WebViewModel can get initialized multiple times, the coordinator will only get initialized once and the viewModel in it does not get updated. Because of that, things break after the first redraw.

Toolbar accessory added to UITextView as a UIViewRepresentable only displays after first launch of iMessage extension application

Would like to have the toolbar show all the time.
Any help is greatly appreciated as this is a real drag for the user experience.
I've added a toolbar to the keyboard for the TextView as shown below.
However the toolbar only shows after the app has run once. Meaning the toolbar does not show the first time the app is run. the app works every time after the initial load.
This is on IOS 14.3, Xcode 12.3, Swift 5, iMessage extension app. Fails on simulator or real device.
struct CustomTextEditor: UIViewRepresentable {
#Binding var text: String
private var returnType: UIReturnKeyType
private var keyType: UIKeyboardType
private var displayDoneBar: Bool
private var commitHandler: (()->Void)?
init(text: Binding<String>,
returnType: UIReturnKeyType = .done,
keyboardType: UIKeyboardType,
displayDoneBar: Bool,
onCommit: (()->Void)?) {
self._text = text
self.returnType = returnType
self.keyType = keyboardType
self.displayDoneBar = displayDoneBar
self.commitHandler = onCommit
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.keyboardType = keyType
textView.returnKeyType = returnType
textView.backgroundColor = .clear
textView.font = UIFont.systemFont(ofSize: 20, weight: .regular)
textView.isEditable = true
textView.delegate = context.coordinator
if self.displayDoneBar {
let flexibleSpace = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace,
target: self,
action: nil)
let doneButton = UIBarButtonItem(title: "Close Keyboard",
style: .done,
target: self,
action: #selector(textView.doneButtonPressed(button:)))
let toolBar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 300, height: 50))
toolBar.items = [flexibleSpace, doneButton, flexibleSpace]
toolBar.setItems([flexibleSpace, doneButton, flexibleSpace], animated: true)
toolBar.sizeToFit()
textView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
textView.translatesAutoresizingMaskIntoConstraints = true
textView.inputAccessoryView = toolBar
}
return textView
}
func updateUIView(_ textView: UITextView, context: Context) {
textView.text = text
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: CustomTextEditor
init(_ textView: CustomTextEditor) {
self.parent = textView
}
func textViewDidChange(_ textView: UITextView) {
self.parent.$text.wrappedValue = textView.text
}
func textViewDidEndEditing(_ textView: UITextView) {
self.parent.$text.wrappedValue = textView.text
parent.commitHandler?()
}
}
}
extension UITextView {
#objc func doneButtonPressed(button:UIBarButtonItem) -> Void {
self.resignFirstResponder()
}
}
This is how it's called...
import SwiftUI
final class ContentViewHostController: UIHostingController<ContentView> {
weak var myWindow: UIWindow?
init() {
super.init(rootView: ContentView())
}
required init?(coder: NSCoder) {
super.init(coder: coder, rootView: ContentView())
}
}
let kTextColor = Color(hex: "3E484F")
let kOverlayRadius: CGFloat = 10
let kOverlayWidth: CGFloat = 2
let kOverlayColor = kTextColor
struct ContentView: View {
#State var text = ""
var body: some View {
VStack {
Spacer()
CustomTextEditor(text: $text, returnType: .default, keyboardType: .default, displayDoneBar: true, onCommit: nil)
.foregroundColor(kTextColor)
.overlay(
RoundedRectangle(cornerRadius: kOverlayRadius)
.stroke(kOverlayColor, lineWidth: kOverlayWidth)
)
.frame(width: 200, height: 100, alignment: .center)
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
From MessagesViewController...
override func willBecomeActive(with conversation: MSConversation) {
let childViewCtrl = ContentViewHostController()
childViewCtrl.view.layoutIfNeeded() // avoids snapshot warning?
if let window = self.view.window {
childViewCtrl.myWindow = window
window.rootViewController = childViewCtrl
}
}

SwiftUI doesn't update state to #ObservedObject cameraViewModel object

I'm new to SwiftUI and manual camera functionality, and I really need help.
So I trying to build a SwiftUI camera view that has a UIKit camera as a wrapper to control the focus lens position via SwiftUI picker view, display below a fucus value, and want to try have a correlation between AVcaptureDevice.lensPosition from 0 to 1.0 and feats that are displayed in the focus picker view. But for now, I only want to display that fucus number on screen.
And the problem is when I try to update focus via coordinator focus observation and set it to the camera view model then nothing happened. Please help 🙌
Here's the code:
import SwiftUI
import AVFoundation
import Combine
struct ContentView: View {
#State private var didTapCapture = false
#State private var focusLensPosition: Float = 0
#ObservedObject var cameraViewModel = CameraViewModel(focusLensPosition: 0)
var body: some View {
VStack {
ZStack {
CameraPreviewRepresentable(didTapCapture: $didTapCapture, cameraViewModel: cameraViewModel)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
VStack {
FocusPicker(selectedFocus: $focusLensPosition)
Text(String(cameraViewModel.focusLensPosition))
.foregroundColor(.red)
.font(.largeTitle)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.edgesIgnoringSafeArea(.all)
Spacer()
CaptureButton(didTapCapture: $didTapCapture)
.frame(width: 100, height: 100, alignment: .center)
.padding(.bottom, 20)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CaptureButton: View {
#Binding var didTapCapture : Bool
var body: some View {
Button {
didTapCapture.toggle()
} label: {
Image(systemName: "photo")
.font(.largeTitle)
.padding(30)
.background(Color.red)
.foregroundColor(.white)
.clipShape(Circle())
.overlay(
Circle()
.stroke(Color.red)
)
}
}
}
struct CameraPreviewRepresentable: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var didTapCapture: Bool
#ObservedObject var cameraViewModel: CameraViewModel
let cameraController: CustomCameraController = CustomCameraController()
func makeUIViewController(context: Context) -> CustomCameraController {
cameraController.delegate = context.coordinator
return cameraController
}
func updateUIViewController(_ cameraViewController: CustomCameraController, context: Context) {
if (self.didTapCapture) {
cameraViewController.didTapRecord()
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self, cameraViewModel: cameraViewModel)
}
class Coordinator: NSObject, UINavigationControllerDelegate, AVCapturePhotoCaptureDelegate {
let parent: CameraPreviewRepresentable
var cameraViewModel: CameraViewModel
var focusLensPositionObserver: NSKeyValueObservation?
init(_ parent: CameraPreviewRepresentable, cameraViewModel: CameraViewModel) {
self.parent = parent
self.cameraViewModel = cameraViewModel
super.init()
focusLensPositionObserver = self.parent.cameraController.currentCamera?.observe(\.lensPosition, options: [.new]) { [weak self] camera, _ in
print(Float(camera.lensPosition))
//announcing changes via Publisher
self?.cameraViewModel.focusLensPosition = camera.lensPosition
}
}
deinit {
focusLensPositionObserver = nil
}
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
parent.didTapCapture = false
if let imageData = photo.fileDataRepresentation(), let image = UIImage(data: imageData) {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
parent.presentationMode.wrappedValue.dismiss()
}
}
}
class CameraViewModel: ObservableObject {
#Published var focusLensPosition: Float = 0
init(focusLensPosition: Float) {
self.focusLensPosition = focusLensPosition
}
}
class CustomCameraController: UIViewController {
var image: UIImage?
var captureSession = AVCaptureSession()
var backCamera: AVCaptureDevice?
var frontCamera: AVCaptureDevice?
var currentCamera: AVCaptureDevice?
var photoOutput: AVCapturePhotoOutput?
var cameraPreviewLayer: AVCaptureVideoPreviewLayer?
//DELEGATE
var delegate: AVCapturePhotoCaptureDelegate?
func showFocusLensPosition() -> Float {
// guard let camera = currentCamera else { return 0 }
// try! currentCamera!.lockForConfiguration()
// currentCamera!.focusMode = .autoFocus
//// currentCamera!.setFocusModeLocked(lensPosition: currentCamera!.lensPosition, completionHandler: nil)
// currentCamera!.unlockForConfiguration()
return currentCamera!.lensPosition
}
func didTapRecord() {
let settings = AVCapturePhotoSettings()
photoOutput?.capturePhoto(with: settings, delegate: delegate!)
}
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
func setup() {
setupCaptureSession()
setupDevice()
setupInputOutput()
setupPreviewLayer()
startRunningCaptureSession()
}
func setupCaptureSession() {
captureSession.sessionPreset = .photo
}
func setupDevice() {
let deviceDiscoverySession =
AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera],
mediaType: .video,
position: .unspecified)
for device in deviceDiscoverySession.devices {
switch device.position {
case .front:
self.frontCamera = device
case .back:
self.backCamera = device
default:
break
}
}
self.currentCamera = self.backCamera
}
func setupInputOutput() {
do {
let captureDeviceInput = try AVCaptureDeviceInput(device: currentCamera!)
captureSession.addInput(captureDeviceInput)
photoOutput = AVCapturePhotoOutput()
captureSession.addOutput(photoOutput!)
} catch {
print(error)
}
}
func setupPreviewLayer() {
self.cameraPreviewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
self.cameraPreviewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
let deviceOrientation = UIDevice.current.orientation
cameraPreviewLayer?.connection?.videoOrientation = AVCaptureVideoOrientation(rawValue: deviceOrientation.rawValue)!
self.cameraPreviewLayer?.frame = self.view.frame
// view.transform = CGAffineTransform(scaleX: 0.5, y: 0.5)
self.view.layer.insertSublayer(cameraPreviewLayer!, at: 0)
}
func startRunningCaptureSession() {
captureSession.startRunning()
}
}
struct FocusPicker: View {
var feets = ["∞ ft", "30", "15", "10", "7", "5", "4", "3.5", "3", "2.5", "2", "1.5", "1", "0.5", "Auto"]
#Binding var selectedFocus: Float
var body: some View {
Picker(selection: $selectedFocus, label: Text("")) {
ForEach(0 ..< feets.count) {
Text(feets[$0])
.foregroundColor(.white)
.font(.subheadline)
.fontWeight(.medium)
}
.animation(.none)
.background(Color.clear)
.pickerStyle(WheelPickerStyle())
}
.frame(width: 60, height: 200)
.border(Color.gray, width: 5)
.clipped()
}
}
The problem with your provided code is that the type of selectedFocus within the FocusPicker view should be Integer rather than Float. So one option is to change this type to Integer and find a way to express the AVCaptureDevice.lensPosition as an Integer with the given range.
The second option is to replace the feets array with an enumeration. By making the enumeration conform to the CustomStringConvertible protocol, you can even provide a proper description. Please see my example below.
I've stripped your code a bit as you just wanted to display the number in the first step and thus the code is more comprehensible.
My working example:
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var cameraViewModel = CameraViewModel(focusLensPosition: 0.5)
var body: some View {
VStack {
ZStack {
VStack {
FocusPicker(selectedFocus: $cameraViewModel.focusLensPosition)
Text(String(self.cameraViewModel.focusLensPosition))
.foregroundColor(.red)
.font(.largeTitle)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.edgesIgnoringSafeArea(.all)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class CameraViewModel: ObservableObject {
#Published var focusLensPosition: Float
init(focusLensPosition: Float) {
self.focusLensPosition = focusLensPosition
}
}
enum Feets: Float, CustomStringConvertible, CaseIterable, Identifiable {
case case1 = 0.0
case case2 = 0.5
case case3 = 1.0
var id: Float { self.rawValue }
var description: String {
get {
switch self {
case .case1:
return "∞ ft"
case .case2:
return "4"
case .case3:
return "Auto"
}
}
}
}
struct FocusPicker: View {
#Binding var selectedFocus: Float
var body: some View {
Picker(selection: $selectedFocus, label: Text("")) {
ForEach(Feets.allCases) { feet in
Text(feet.description)
}
.animation(.none)
.background(Color.clear)
.pickerStyle(WheelPickerStyle())
}
.frame(width: 60, height: 200)
.border(Color.gray, width: 5)
.clipped()
}
}

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.

SwiftUI How can I add an activity indicator in WKWebView?

How can I add an activity indicator in WKWebView which will display the indicator while the webpage is loading and disappears when loaded ?
I've looked at some of the old posts but could not figure out how to do it in SwiftUI
see link to one of the old solutions below
How to add Activity Indicator to WKWebView (Swift 3)
Use UIViewRepresentable to create a UIActivityIndicatorView:
You control when an activity indicator animates by calling the startAnimating() and stopAnimating() methods. To automatically hide the activity indicator when animation stops, set the hidesWhenStopped property to true.
You can set the color of the activity indicator by using the color property.
struct ActivityIndicatorView: UIViewRepresentable {
#Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
func makeUIView(context: UIViewRepresentableContext<ActivityIndicatorView>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicatorView>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
Create a LoadingView to allow you to wrap around your views:
This allows you to style the activity views content.
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text("Loading...")
ActivityIndicatorView(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2, height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.red)
.cornerRadius(20)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
If you want to be able to update the LoadingView(...) status you'll need to introduce a view model that inherits from ObservableObject:
Based on this answer: https://stackoverflow.com/a/58825642/264802
class WebViewModel: ObservableObject {
#Published var url: String
#Published var isLoading: Bool = true
init (url: String) {
self.url = url
}
}
struct WebView: UIViewRepresentable {
#ObservedObject var viewModel: WebViewModel
let webView = WKWebView()
func makeCoordinator() -> Coordinator {
Coordinator(self.viewModel)
}
class Coordinator: NSObject, WKNavigationDelegate {
private var viewModel: WebViewModel
init(_ viewModel: WebViewModel) {
self.viewModel = viewModel
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.viewModel.isLoading = false
}
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<WebView>) { }
func makeUIView(context: Context) -> UIView {
self.webView.navigationDelegate = context.coordinator
if let url = URL(string: self.viewModel.url) {
self.webView.load(URLRequest(url: url))
}
return self.webView
}
}
Then to use it inside your views you would do the following:
struct ContentView: View {
#StateObject var model = WebViewModel(url: "http://www.google.com")
var body: some View {
LoadingView(isShowing: self.$model.isLoading) {
WebView(viewModel: self.model)
}
}
}
Using 3 Steps I do it in my project.
Step 1: Create a Loading View
import SwiftUI
import UIKit
struct ActivityIndicatorView: UIViewRepresentable {
#Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
func makeUIView(context: Context) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
// Main View
struct LoadingView<Content>: View where Content: View {
#Binding var isShowing: Bool
let message: String
var content: () -> Content
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
self.content()
.disabled(self.isShowing)
.blur(radius: self.isShowing ? 3 : 0)
VStack {
Text(self.message)
.bold()
ActivityIndicatorView(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.opacity(self.isShowing ? 1 : 0)
}
}
}
}
// Mark: Testing
struct LoadingIndicator: View {
var body: some View {
LoadingView(isShowing: .constant(true), message: "Loading...") {
NavigationView {
List(["1", "2", "3", "4", "5"], id: \.self) { row in
Text(row)
}.navigationBarTitle(Text("A List"), displayMode: .large)
}
}
}
}
struct ActivityIndicatorView_Previews: PreviewProvider {
static var previews: some View {
LoadingIndicator()
}
}
Step 2: Create a WebView and WebViewModel
import SwiftUI
import WebKit
class WebViewModel: ObservableObject {
#Published var isLoading: Bool = false
}
struct WebView: UIViewRepresentable {
#ObservedObject var webViewModel: WebViewModel
let urlString: String
func makeUIView(context: Context) -> WKWebView {
let wkWebView = WKWebView()
if let url = URL(string: urlString) {
let urlRequest = URLRequest(url: url)
wkWebView.load(urlRequest)
}
return wkWebView
}
func updateUIView(_ wkWebView: WKWebView, context: Context) {
// do nothing
}
class Coordinator: NSObject, WKNavigationDelegate {
let webViewModel: WebViewModel
init(_ webViewModel: WebViewModel) {
self.webViewModel = webViewModel
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
webViewModel.isLoading = true
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webViewModel.isLoading = false
}
}
func makeCoordinator() -> WebView.Coordinator {
Coordinator(webViewModel)
}
}
struct WebView_Previews: PreviewProvider {
static var previews: some View {
WebView(webViewModel: WebViewModel(),
urlString: "https://instagram.com/mahmudahsan/")
}
}
Step 3: In your main view use the following code to show indicator and webview
ZStack {
WebView(webViewModel: webViewModel, urlString: "http://ithinkdiff.net")
.frame(height: 1000)
if webViewModel.isLoading {
LoadingView(isShowing: .constant(true), message: "Loading...") {
EmptyView()
}
}
}