Exposing an instance method to Objective-C within an extension method - xcode

I've got an application that uses UIKeyboardWillShow & Hide notifications in a few different controllers. I've decided to try to consolidate the logic required to move the view with the keyboard into an extension based on a protocol.
Here's my protocol
public protocol KeyboardType : class {
func keyboardWillShow(_ sender: Notification)
func keyboardWillHide(_ sender: Notification)
}
Next, I added an extension to my new protocol so that all I need to do is implement my "KeyboardType protocol and I'll gain access to the necessary functionality to move my view with my keyboard:
Here's my extension
public extension KeyboardType where Self: UIViewController {
func addObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(Self.keyboardWillShow(_:)), name:NSNotification.Name.UIKeyboardWillShow, object: self.view.window)
NotificationCenter.default.addObserver(self, selector: #selector(Self.keyboardWillHide(_:)), name:NSNotification.Name.UIKeyboardWillHide, object: self.view.window)
}
func removeObservers() {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: self.view.window)
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: self.view.window)
}
func keyboardWillHide(_ sender: Notification) {
let userInfo: [AnyHashable : Any] = (sender as NSNotification).userInfo!
let keyboardSize: CGSize = (userInfo[UIKeyboardFrameBeginUserInfoKey]! as AnyObject).cgRectValue.size
self.view.frame.origin.y += keyboardSize.height
}
func keyboardWillShow(_ sender: Notification) {
let userInfo: [AnyHashable : Any] = sender.userInfo!
let keyboardSize: CGSize = (userInfo[UIKeyboardFrameBeginUserInfoKey]! as AnyObject).cgRectValue.size
let offset: CGSize = (userInfo[UIKeyboardFrameEndUserInfoKey]! as AnyObject).cgRectValue.size
if keyboardSize.height == offset.height {
if self.view.frame.origin.y == 0 {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.view.frame.origin.y -= keyboardSize.height
})
}
} else {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.view.frame.origin.y += keyboardSize.height - offset.height
})
}
}
}
The problem
The problem is the compiler is asking me to add #objc to my keyboardWillShow and keyboardWillHide methods. When I allow Xcode to add the keywords, the compiler immediately asks me to remove the #objc keywords.
Argument of '#selector' refers to instance method 'keyboardWillShow'
that is not exposed to Objective-C
My Question
How do I expose keyboardWillShow to Objective-C in this situation?
or
Is there a better way to accomplish the same task?

I would take a different approach there extending UIViewController as follow:
protocol KeyboardController {
func keyboardWillShow(_ sender: Notification)
func keyboardWillHide(_ sender: Notification)
}
extension UIViewController: KeyboardController {
func addObservers() {
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow(_:)),
name:.UIKeyboardWillShow,
object: view.window)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillHide(_:)),
name:.UIKeyboardWillHide,
object: view.window)
}
func removeObservers() {
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: view.window)
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: view.window)
}
func keyboardWillHide(_ notification: Notification) {
print("---> keyboardWillHide")
guard let userInfo = notification.userInfo else { return }
let keyboardSize = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size
print("keyboardSize", keyboardSize)
}
func keyboardWillShow(_ notification: Notification) {
print("---> keyboardWillShow")
guard
let userInfo = notification.userInfo,
let keyboardSize = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size,
let offset = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size
else { return }
if keyboardSize.height == offset.height {
if self.view.frame.origin.y == 0 {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.view.frame.origin.y -= keyboardSize.height
})
}
} else {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.view.frame.origin.y += keyboardSize.height - offset.height
})
}
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
addObservers()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Another option and the one I like most is to subclass UIViewController:
class KeyboardViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
addObservers()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
removeObservers()
}
func addObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: .UIKeyboardWillShow, object: view.window)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: .UIKeyboardWillHide, object: view.window)
}
func removeObservers() {
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: view.window)
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: view.window)
}
func keyboardWillHide(_ notification: Notification) {
print("---> keyboardWillHide")
if let keyboardHeight = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size.height {
print("keyboardHeight", keyboardHeight)
view.frame.origin.y += keyboardHeight
}
}
func keyboardWillShow(_ notification: Notification) {
print("---> keyboardWillShow")
if let userInfo = notification.userInfo,
let keyboardHeight = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue.size.height,
let offsetHeight = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height {
print("userInfo", userInfo)
print("keyboardHeight", keyboardHeight)
print("offsetHeight", offsetHeight)
if keyboardHeight == offsetHeight {
if self.view.frame.origin.y == 0 {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.view.frame.origin.y -= keyboardHeight
})
}
} else {
UIView.animate(withDuration: 0.1, animations: { () -> Void in
self.view.frame.origin.y += keyboardHeight - offsetHeight
})
}
}
}
}
class ViewController: KeyboardViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

Have you tried this?:
#objc public protocol KeyboardType {
func keyboardWillShow(_ sender: Notification)
func keyboardWillHide(_ sender: Notification)
}
You also need to import UIKit

Related

Can't pass the RxSwift PublishRelay value from custom view

I am trying to extend my KeyboardView view with rx action with no success. According to debugging with breakpoints value is passed to the relay but extension is not called despite further subscription in a view controller. What might be a problem and how to fix it?
final class KeyboardView: UIView {
private let disposeBag = DisposeBag()
fileprivate let buttonTappedRelay = PublishRelay<ActionType>()
private let digitButtons: [KeyboardButton] = {
return stride(from: 0, through: 9, by: 1)
.compactMap { $0 }
.map { KeyboardButton(actionType: .digit($0)) }
}()
private let eraseButton: KeyboardButton = {
let button = KeyboardButton(actionType: .erase)
return button
}()
public override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupConstraints()
setupActions()
}
private func setupViews() { ... }
private func setupConstraints() { ... }
private func setupActions() {
eraseButton.rx.buttonTap
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] actionType in
self?.buttonTappedRelay.accept(actionType)
}).disposed(by: self.disposeBag)
for button in digitButtons {
button.rx.buttonTap
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] actionType in
self?.buttonTappedRelay.accept(actionType)
}).disposed(by: self.disposeBag)
}
}
}
extension Reactive where Base: KeyboardView {
internal var buttonTap: ControlEvent<ActionType> {
return ControlEvent<ActionType>(events: base.buttonTappedRelay.asObservable() )
}
}
Your problem is likely in code you haven't shown. Note that the below code compiles, runs and works:
final class KeyboardView: UIView {
private let disposeBag = DisposeBag()
fileprivate let buttonTappedRelay = PublishRelay<ActionType>()
private let digitButtons: [KeyboardButton] = {
return stride(from: 0, through: 9, by: 1)
.compactMap { $0 }
.map { KeyboardButton(actionType: .digit($0)) }
}()
private let eraseButton: KeyboardButton = {
let button = KeyboardButton(actionType: .erase)
return button
}()
public override init(frame: CGRect) {
super.init(frame: frame)
setupViews()
setupActions()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupViews() {
let stack = UIStackView(frame: bounds)
stack.distribution = .equalSpacing
stack.addArrangedSubview(eraseButton)
for each in digitButtons {
stack.addArrangedSubview(each)
}
addSubview(stack)
}
private func setupActions() {
eraseButton.rx.buttonTap
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] actionType in
self?.buttonTappedRelay.accept(actionType)
}).disposed(by: self.disposeBag)
for button in digitButtons {
button.rx.buttonTap
.asObservable()
.observeOn(MainScheduler.instance)
.subscribe(onNext: { [weak self] actionType in
self?.buttonTappedRelay.accept(actionType)
}).disposed(by: self.disposeBag)
}
}
}
final class ViewController: UIViewController {
weak var keyboard: KeyboardView?
override func loadView() {
super.loadView()
let keyboard = KeyboardView(frame: view.bounds)
view.addSubview(keyboard)
self.keyboard = keyboard
}
override func viewDidLoad() {
super.viewDidLoad()
keyboard!.rx.buttonTap
.debug("🟣")
.subscribe()
}
}
extension Reactive where Base: KeyboardView {
internal var buttonTap: ControlEvent<ActionType> {
return ControlEvent<ActionType>(events: base.buttonTappedRelay.asObservable() )
}
}
enum ActionType {
case digit(Int)
case erase
}
class KeyboardButton: UIButton {
let actionType: ActionType
init(actionType: ActionType) {
self.actionType = actionType
super.init(frame: CGRect.zero)
backgroundColor = .red
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension Reactive where Base: KeyboardButton {
var buttonTap: Observable<ActionType> {
base.rx.tap.map { base.actionType }
}
}

NSButton inside a NSViewRepresentable gives no "visual feedback" if clicked

I built a NativeButton based on a NSButton with a NSViewRepresentable to detect Rightclicks.
It works fine, but I do not get "visual feedback" as with a normal Button, if it's clicked.
How can I accomplish that the Button get dark, if it's clicked (as any normal Button)?
struct TEST_NativeButton: View {
var body: some View {
VStack{
Text("Test Native Button")
NativeButton(
rightClickAction: {print("rightclick")},
leftClickAction: {print("leftclick")}
)
{ print("standard action")
}
}.frame(width: 200, height: 200)
}
}
struct NativeButton: NSViewRepresentable {
typealias NSViewType = NativeNSButton
let rightClickAction: ActionFunction
let leftClickAction: ActionFunction
let standardAction: ActionFunction
init(
rightClickAction : ActionFunction? = nil,
leftClickAction : ActionFunction? = nil,
standardAction: #escaping ActionFunction
)
{
if let rightClickAction = rightClickAction{
self.rightClickAction = rightClickAction
} else {
self.rightClickAction = {}
}
if let leftClickAction = leftClickAction{
self.leftClickAction = leftClickAction
} else {
self.leftClickAction = standardAction
}
self.standardAction = standardAction
}
func makeNSView(context: Context) -> NativeNSButton {
NativeNSButton(
rightClickAction: rightClickAction,
leftClickAction: leftClickAction,
standardAction: standardAction)
}
func updateNSView(_ nsView: NativeNSButton, context: Context) {
//ToDo auf änderungen am Titel reagieren
return
}
}
class NativeNSButton: NSButton {
let standardAction: () -> Void
let rightClickAction: () -> Void
let leftClickAction: () -> Void
init(
rightClickAction : #escaping () -> Void,
leftClickAction : #escaping () -> Void,
standardAction: #escaping () -> Void) {
self.standardAction = standardAction
self.rightClickAction = rightClickAction
self.leftClickAction = leftClickAction
super.init(frame: .zero)
self.title = title
self.alignment = alignment
self.target = self
self.action = #selector(clickButton(_:))
bezelStyle = .rounded
isBordered = true
focusRingType = .none
self.translatesAutoresizingMaskIntoConstraints = false
self.setContentHuggingPriority(.defaultHigh, for: .vertical)
self.setContentHuggingPriority(.defaultHigh, for: .horizontal)
}
required init?(coder: NSCoder) {
fatalError()
}
override func mouseDown(with theEvent: NSEvent) {
//print("left mouse")
leftClickAction()
}
override func rightMouseDown(with theEvent: NSEvent) {
//print("right mouse")
rightClickAction()
}
#objc func clickButton(_ sender: BbcNSButton) {
//print("standard action")
standardAction()
}
}
The standard action is used for detection Keyboard-Shortcuts (removed in this example).
Call super method
override func mouseDown(with theEvent: NSEvent) {
super.mouseDown(with: theEvent)
//print("left mouse")
leftClickAction()
}

SwiftUI on Mac - How do I designate a button as being the primary?

In AppKit I would do this by assigning its key equivalent to be ↩ or making its cell the window's default. However, neither of these seems possible in SwiftUI, so how do I make a button the default window button?
macOS 11.0 / iOS 14:
As of Xcode 12 beta, new methods are exposed on Button() allowing assignment of keyEquivalent (either by enum case or explicit key and modifiers).
Setting as default:
Button( ... )
.keyboardShortcut(.defaultAction)
Setting as cancel:
Button( ... )
.keyboardShortcut(.cancelAction)
It's currently not possible. I have reported it to Apple.
However, for now, you can wrap NSButton.
Usage:
#available(macOS 10.15, *)
struct ContentView: View {
var body: some View {
NativeButton("Submit", keyEquivalent: .return) {
// Some action
}
.padding()
}
}
Implementation:
// MARK: - Action closure for controls
private var controlActionClosureProtocolAssociatedObjectKey: UInt8 = 0
protocol ControlActionClosureProtocol: NSObjectProtocol {
var target: AnyObject? { get set }
var action: Selector? { get set }
}
private final class ActionTrampoline<T>: NSObject {
let action: (T) -> Void
init(action: #escaping (T) -> Void) {
self.action = action
}
#objc
func action(sender: AnyObject) {
action(sender as! T)
}
}
extension ControlActionClosureProtocol {
func onAction(_ action: #escaping (Self) -> Void) {
let trampoline = ActionTrampoline(action: action)
self.target = trampoline
self.action = #selector(ActionTrampoline<Self>.action(sender:))
objc_setAssociatedObject(self, &controlActionClosureProtocolAssociatedObjectKey, trampoline, .OBJC_ASSOCIATION_RETAIN)
}
}
extension NSControl: ControlActionClosureProtocol {}
// MARK: -
#available(macOS 10.15, *)
struct NativeButton: NSViewRepresentable {
enum KeyEquivalent: String {
case escape = "\u{1b}"
case `return` = "\r"
}
var title: String?
var attributedTitle: NSAttributedString?
var keyEquivalent: KeyEquivalent?
let action: () -> Void
init(
_ title: String,
keyEquivalent: KeyEquivalent? = nil,
action: #escaping () -> Void
) {
self.title = title
self.keyEquivalent = keyEquivalent
self.action = action
}
init(
_ attributedTitle: NSAttributedString,
keyEquivalent: KeyEquivalent? = nil,
action: #escaping () -> Void
) {
self.attributedTitle = attributedTitle
self.keyEquivalent = keyEquivalent
self.action = action
}
func makeNSView(context: NSViewRepresentableContext<Self>) -> NSButton {
let button = NSButton(title: "", target: nil, action: nil)
button.translatesAutoresizingMaskIntoConstraints = false
button.setContentHuggingPriority(.defaultHigh, for: .vertical)
button.setContentHuggingPriority(.defaultHigh, for: .horizontal)
return button
}
func updateNSView(_ nsView: NSButton, context: NSViewRepresentableContext<Self>) {
if attributedTitle == nil {
nsView.title = title ?? ""
}
if title == nil {
nsView.attributedTitle = attributedTitle ?? NSAttributedString(string: "")
}
nsView.keyEquivalent = keyEquivalent?.rawValue ?? ""
nsView.onAction { _ in
self.action()
}
}
}
Here is a shorter, but less generic solution to create a primary button with return key equivalent, and default button blue tinting.
struct PrimaryButtonView: NSViewRepresentable {
typealias NSViewType = PrimaryButton
let title: String
let action: () -> Void
init(_ title: String, action: #escaping () -> Void) {
self.title = title
self.action = action
}
func makeNSView(context: Context) -> PrimaryButton {
PrimaryButton(title, action: action)
}
func updateNSView(_ nsView: PrimaryButton, context: Context) {
return
}
}
class PrimaryButton: NSButton {
let buttonAction: () -> Void
init(_ title: String, action: #escaping () -> Void) {
self.buttonAction = action
super.init(frame: .zero)
self.title = title
self.action = #selector(clickButton(_:))
bezelStyle = .rounded //Only this style results in blue tint for button
isBordered = true
focusRingType = .none
keyEquivalent = "\r"
}
required init?(coder: NSCoder) {
fatalError()
}
#objc func clickButton(_ sender: PrimaryButton) {
buttonAction()
}
}

Showing 'UIActivityViewController' in SwiftUI

I want to let the user to be able to share a location but I don't know how to show UIActivityViewController in SwiftUI.
The basic implementation of UIActivityViewController in SwiftUI is
import UIKit
import SwiftUI
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [Any]
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
}
And here is how to use it.
struct MyView: View {
#State private var isSharePresented: Bool = false
var body: some View {
Button("Share app") {
self.isSharePresented = true
}
.sheet(isPresented: $isSharePresented, onDismiss: {
print("Dismiss")
}, content: {
ActivityViewController(activityItems: [URL(string: "https://www.apple.com")!])
})
}
}
Based on Tikhonov's, the following code added a fix to make sure the activity sheet is dismissed properly (if not subsequently the sheet will not be presented).
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [Any]
var applicationActivities: [UIActivity]? = nil
#Environment(\.presentationMode) var presentationMode
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
self.presentationMode.wrappedValue.dismiss()
}
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
}
It's a one time thing currently. .sheet will show it as a sheet, but bringing it up again from the same view will have stale data. Those subsequent shows of the sheet will also not trigger any completion handlers. Basically, makeUIViewController is called only once which is the only way to get the data to share into the UIActivityViewController. updateUIViewController has no way to update the data in your activityItems or reset the controller because those are not visible from an instance of UIActivityViewController.
Note that it doesn't work with UIActivityItemSource or UIActivityItemProvider either. Using those is even worse. The placeholder value doesn't show.
I hacked around some more and decided that maybe the problem with my solution was a sheet that was presenting another sheet, and when one went away then the other stayed.
This indirect way of having a ViewController do the presentation when it appears made it work for me.
class UIActivityViewControllerHost: UIViewController {
var message = ""
var completionWithItemsHandler: UIActivityViewController.CompletionWithItemsHandler? = nil
override func viewDidAppear(_ animated: Bool) {
share()
}
func share() {
// set up activity view controller
let textToShare = [ message ]
let activityViewController = UIActivityViewController(activityItems: textToShare, applicationActivities: nil)
activityViewController.completionWithItemsHandler = completionWithItemsHandler
activityViewController.popoverPresentationController?.sourceView = self.view // so that iPads won't crash
// present the view controller
self.present(activityViewController, animated: true, completion: nil)
}
}
struct ActivityViewController: UIViewControllerRepresentable {
#Binding var text: String
#Binding var showing: Bool
func makeUIViewController(context: Context) -> UIActivityViewControllerHost {
// Create the host and setup the conditions for destroying it
let result = UIActivityViewControllerHost()
result.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
// To indicate to the hosting view this should be "dismissed"
self.showing = false
}
return result
}
func updateUIViewController(_ uiViewController: UIActivityViewControllerHost, context: Context) {
// Update the text in the hosting controller
uiViewController.message = text
}
}
struct ContentView: View {
#State private var showSheet = false
#State private var message = "a message"
var body: some View {
VStack {
TextField("what to share", text: $message)
Button("Hello World") {
self.showSheet = true
}
if showSheet {
ActivityViewController(text: $message, showing: $showSheet)
.frame(width: 0, height: 0)
}
Spacer()
}
.padding()
}
}
May be its not recommended, but it is really easy and two line of code (was for iPhone) to share text
Button(action: {
let shareActivity = UIActivityViewController(activityItems: ["Text To Share"], applicationActivities: nil)
if let vc = UIApplication.shared.windows.first?.rootViewController{
shareActivity.popoverPresentationController?.sourceView = vc.view
//Setup share activity position on screen on bottom center
shareActivity.popoverPresentationController?.sourceRect = CGRect(x: UIScreen.main.bounds.width / 2, y: UIScreen.main.bounds.height, width: 0, height: 0)
shareActivity.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.down
vc.present(shareActivity, animated: true, completion: nil)
}
}) {
Text("Share")
}
EDIT: Now works fine on iPad (tested on iPad Pro (9.7 -inch) Simulator)
I want to suggest another implementation that looks more native (half screen height without white gap bottom).
import SwiftUI
struct ActivityView: UIViewControllerRepresentable {
var activityItems: [Any]
var applicationActivities: [UIActivity]? = nil
#Binding var isPresented: Bool
func makeUIViewController(context: Context) -> ActivityViewWrapper {
ActivityViewWrapper(activityItems: activityItems, applicationActivities: applicationActivities, isPresented: $isPresented)
}
func updateUIViewController(_ uiViewController: ActivityViewWrapper, context: Context) {
uiViewController.isPresented = $isPresented
uiViewController.updateState()
}
}
class ActivityViewWrapper: UIViewController {
var activityItems: [Any]
var applicationActivities: [UIActivity]?
var isPresented: Binding<Bool>
init(activityItems: [Any], applicationActivities: [UIActivity]? = nil, isPresented: Binding<Bool>) {
self.activityItems = activityItems
self.applicationActivities = applicationActivities
self.isPresented = isPresented
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)
updateState()
}
fileprivate func updateState() {
guard parent != nil else {return}
let isActivityPresented = presentedViewController != nil
if isActivityPresented != isPresented.wrappedValue {
if !isActivityPresented {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
controller.completionWithItemsHandler = { (activityType, completed, _, _) in
self.isPresented.wrappedValue = false
}
present(controller, animated: true, completion: nil)
}
else {
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
}
}
struct ActivityViewTest: View {
#State private var isActivityPresented = false
var body: some View {
Button("Preset") {
self.isActivityPresented = true
}.background(ActivityView(activityItems: ["Hello, World"], isPresented: $isActivityPresented))
}
}
struct ActivityView_Previews: PreviewProvider {
static var previews: some View {
ActivityViewTest()
}
}
I got it to work now using
.sheet(isPresented: $isSheet, content: { ActivityViewController() }
.presentation is deprecated
It takes up the whole screen iOS 13 style.
If you need more granular control over the content displayed in the share sheet, you will probably end implementing UIActivityItemSource.
I tried using Mike W.'s code above but it didn't work at first (the delegate functions weren't being called). The fix was changing the initialisation of UIActivityController within makeUIViewController as follows, now passing [context.coordinator] as activityItems:
let controller = UIActivityViewController(activityItems: [context.coordinator], applicationActivities: applicationActivities)
Also, I wanted to be able to set the icon, title and subtitle in the share sheet, so I have implemented func activityViewControllerLinkMetadata in the Coordinator class.
The following is the complete expanded version of Mike W.'s answer. Please note you will need to add import LinkPresentation to the code.
ActivityViewController
import SwiftUI
import LinkPresentation
struct ActivityViewController: UIViewControllerRepresentable {
var shareable : ActivityShareable?
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: [context.coordinator], applicationActivities: applicationActivities)
controller.modalPresentationStyle = .automatic
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
func makeCoordinator() -> ActivityViewController.Coordinator {
Coordinator(self.shareable)
}
class Coordinator : NSObject, UIActivityItemSource {
private let shareable : ActivityShareable?
init(_ shareable: ActivityShareable?) {
self.shareable = shareable
super.init()
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
guard let share = self.shareable else { return "" }
return share.getPlaceholderItem()
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
guard let share = self.shareable else { return "" }
return share.itemForActivityType(activityType: activityType)
}
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
guard let share = self.shareable else { return "" }
return share.subjectForActivityType(activityType: activityType)
}
func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? {
guard let share = self.shareable else { return nil }
let metadata = LPLinkMetadata()
// share sheet preview title
metadata.title = share.shareSheetTitle()
// share sheet preview subtitle
metadata.originalURL = URL(fileURLWithPath: share.shareSheetSubTitle())
// share sheet preview icon
if let image = share.shareSheetIcon() {
let imageProvider = NSItemProvider(object: image)
metadata.imageProvider = imageProvider
metadata.iconrovider = imageProvider
}
return metadata
}
}
}
Protocol ActivityShareable
protocol ActivityShareable {
func getPlaceholderItem() -> Any
func itemForActivityType(activityType: UIActivity.ActivityType?) -> Any?
func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String
func shareSheetTitle() -> String
func shareSheetSubTitle() -> String
func shareSheetIcon() -> UIImage?
}
In my case I am using the share sheet to export text, so I created a struct called ActivityShareableText that conforms to ActivityShareable:
struct ActivityShareableText: ActivityShareable {
let text: String
let title: String
let subTitle: String
let icon: UIImage?
func getPlaceholderItem() -> Any {
return text
}
func itemForActivityType(activityType: UIActivity.ActivityType?) -> Any? {
return text
}
func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String {
return "\(title): \(subTitle)"
}
func shareSheetTitle() -> String {
return title
}
func shareSheetSubTitle() -> String {
return subTitle
}
func shareSheetIcon() -> UIImage? {
return icon
}
}
In my code, I call the share sheet as follows:
ActivityViewController(shareable: ActivityShareableText(
text: myShareText(),
title: myShareTitle(),
subTitle: myShareSubTitle(),
icon: UIImage(named: "myAppLogo")
))
FWIW - Providing a slight improvement to answers that includes an implementation for UIActivityItemSource. Code simplified for brevity, specifically around the default return on itemForActivityType and activityViewControllerPlaceholderItem, they must always return the same type.
ActivityViewController
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [Any]
var shareable : ActivityShareable?
var applicationActivities: [UIActivity]? = nil
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>) -> UIActivityViewController {
let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
controller.modalPresentationStyle = .automatic
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: UIViewControllerRepresentableContext<ActivityViewController>) {}
func makeCoordinator() -> ActivityViewController.Coordinator {
Coordinator(self.shareable)
}
class Coordinator : NSObject, UIActivityItemSource {
private let shareable : ActivityShareable?
init(_ shareable: ActivityShareable?) {
self.shareable = shareable
super.init()
}
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
guard let share = self.shareable else { return "" }
return share.getPlaceholderItem()
}
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
guard let share = self.shareable else { return "" }
return share.itemForActivityType(activityType: activityType)
}
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
guard let share = self.shareable else { return "" }
return share.subjectForActivityType(activityType: activityType)
}
}
}
ActivityShareable
protocol ActivityShareable {
func getPlaceholderItem() -> Any
func itemForActivityType(activityType: UIActivity.ActivityType?) -> Any?
/// Optional
func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String
}
extension ActivityShareable {
func subjectForActivityType(activityType: UIActivity.ActivityType?) -> String {
return ""
}
}
You could pass in the reference for ActivityViewController or the underlying UIActivityViewController but that feels unnecessary.
You could try porting UIActivityViewController to SwiftUI as follows:
struct ActivityView: UIViewControllerRepresentable {
let activityItems: [Any]
let applicationActivities: [UIActivity]?
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityView>) -> UIActivityViewController {
return UIActivityViewController(activityItems: activityItems,
applicationActivities: applicationActivities)
}
func updateUIViewController(_ uiViewController: UIActivityViewController,
context: UIViewControllerRepresentableContext<ActivityView>) {
}
}
but the app will crash when you try to display it.
I tried: Modal, Popover and NavigationButton.
To test it:
struct ContentView: View {
var body: some Body {
EmptyView
.presentation(Modal(ActivityView()))
}
}
It doesn't seem to be usable from SwiftUI.
Extending upon #Shimanski Artem solution. I think we can write that code more concise. So I basically embed my ActivityViewController in a blank UIViewController and present it from there. This way we don't get the full 'overlay' sheet and you get the native behaviour. Just like #Shimanski Artem did.
struct UIKitActivityView: UIViewControllerRepresentable {
#Binding var isPresented: Bool
let data: [Any]
func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
let activityViewController = UIActivityViewController(
activityItems: data,
applicationActivities: nil
)
if isPresented && uiViewController.presentedViewController == nil {
uiViewController.present(activityViewController, animated: true)
}
activityViewController.completionWithItemsHandler = { (_, _, _, _) in
isPresented = false
}
}
}
Usage
struct ActivityViewTest: View {
#State private var isActivityPresented = false
var body: some View {
Button("Preset") {
self.isActivityPresented = true
}
.background(
UIKitActivityView(
isPresented: $viewModel.showShareSheet,
data: ["String"]
)
)
}
}
Example using SwiftUIX
There is a library called SwiftUIX that already has a wrapper for UIActivityViewController. See quick skeleton of how to present it via .sheet() which should be placed somewhere in the var body: some View {}.
import SwiftUIX
/// ...
#State private var showSocialsInviteShareSheet: Bool = false
// ...
.sheet(isPresented: $showSocialsInviteShareSheet, onDismiss: {
print("Dismiss")
}, content: {
AppActivityView(activityItems: [URL(string: "https://www.apple.com")!])
})
Suggest another way to solve it 🤔
You can create the Empty View Controller to present the sheet
struct ShareSheet: UIViewControllerRepresentable {
// To setup the share sheet
struct Config {
let activityItems: [Any]
var applicationActivities: [UIActivity]?
var excludedActivityTypes: [UIActivity.ActivityType]?
}
// Result object
struct Result {
let error: Error?
let activityType: UIActivity.ActivityType?
let completed: Bool
let returnedItems: [Any]?
}
#Binding var isPresented: Bool
private var handler: ((Result) -> Void)?
private let shareSheet: UIActivityViewController
init(
isPresented: Binding<Bool>,
config: Config,
onEnd: ((Result) -> Void)? = nil
) {
self._isPresented = isPresented
shareSheet = UIActivityViewController(
activityItems: config.activityItems,
applicationActivities: config.applicationActivities
)
shareSheet.excludedActivityTypes = config.excludedActivityTypes
shareSheet.completionWithItemsHandler = { activityType, completed, returnedItems, error in
onEnd?(
.init(
error: error,
activityType: activityType,
completed: completed,
returnedItems: returnedItems
)
)
// Set isPresented to false after complete
isPresented.wrappedValue = false
}
}
func makeUIViewController(context: Context) -> UIViewController {
UIViewController()
}
func updateUIViewController(
_ uiViewController: UIViewController,
context: Context
) {
if isPresented, shareSheet.view.window == nil {
uiViewController.present(shareSheet, animated: true, completion: nil)
} else if !isPresented, shareSheet.view.window != nil {
shareSheet.dismiss(animated: true)
}
}
}
You can also create the operator in the view extension
extension View {
func shareSheet(
isPresented: Binding<Bool>,
config: ShareSheet.Config,
onEnd: ((ShareSheet.Result) -> Void)? = nil
) -> some View {
self.background(
ShareSheet(isPresented: isPresented, config: config, onEnd: onEnd)
)
}
}
Thanks for the helpful answers in this thread.
I tried to solve the stale data problem. The issue from not not implementing updateUIViewController in UIViewControllerRepresentable. SwiftUI calls makeUIViewController only once to create the view controller. The method updateUIViewController is responsible to make changes to view controller based on changes of the SwiftUI view.
As UIActivityViewController does not allow to change activityItems and applicationActivities, I used a wrapper view controller. UIViewControllerRepresentable will update the wrapper and the wrapper will create a new UIActivityViewController as needed to perform the update.
Below my code to implement a "share" button in my application. The code is tested on iOS 13.4 beta, which has fixed several SwiftUI bugs - not sure if it works on earlier releases.
struct Share: View {
var document: ReaderDocument // UIDocument subclass
#State var showShareSheet = false
var body: some View {
Button(action: {
self.document.save(to: self.document.fileURL, for: .forOverwriting) { success in
self.showShareSheet = true
}
}) {
Image(systemName: "square.and.arrow.up")
}.popover(isPresented: $showShareSheet) {
ActivityViewController(activityItems: [ self.document.text, self.document.fileURL,
UIPrintInfo.printInfo(), self.printFormatter ])
.frame(minWidth: 320, minHeight: 500) // necessary for iPad
}
}
var printFormatter: UIPrintFormatter {
let fontNum = Preferences.shared.readerFontSize.value
let fontSize = ReaderController.readerFontSizes[fontNum < ReaderController.readerFontSizes.count ? fontNum : 1]
let printFormatter = UISimpleTextPrintFormatter(text: self.document.text)
printFormatter.startPage = 0
printFormatter.perPageContentInsets = UIEdgeInsets(top: 72, left: 72, bottom: 72, right: 72)
return printFormatter
}
}
struct ActivityViewController: UIViewControllerRepresentable {
var activityItems: [Any]
var applicationActivities: [UIActivity]? = nil
#Environment(\.presentationMode) var presentationMode
func makeUIViewController(context: UIViewControllerRepresentableContext<ActivityViewController>)
-> WrappedViewController<UIActivityViewController> {
let controller = WrappedViewController(wrappedController: activityController)
return controller
}
func updateUIViewController(_ uiViewController: WrappedViewController<UIActivityViewController>,
context: UIViewControllerRepresentableContext<ActivityViewController>) {
uiViewController.wrappedController = activityController
}
private var activityController: UIActivityViewController {
let avc = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities)
avc.completionWithItemsHandler = { (_, _, _, _) in
self.presentationMode.wrappedValue.dismiss()
}
return avc
}
}
class WrappedViewController<Controller: UIViewController>: UIViewController {
var wrappedController: Controller {
didSet {
if (wrappedController != oldValue) {
oldValue.removeFromParent()
oldValue.view.removeFromSuperview()
addChild(wrappedController)
view.addSubview(wrappedController.view)
wrappedController.view.frame = view.bounds
}
}
}
init(wrappedController: Controller) {
self.wrappedController = wrappedController
super.init(nibName: nil, bundle: nil)
addChild(wrappedController)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadView() {
super.loadView()
view.addSubview(wrappedController.view)
wrappedController.view.frame = view.bounds
}
}
Just use introspect. Then you can easily code something like this:
YourView().introspectViewController { controller in
guard let items = viewModel.inviteLinkParams, viewModel.isSharePresented else { return }
let activity = UIActivityViewController(activityItems: items, applicationActivities: nil)
controller.present(activity, animated: true, completion: nil)
}
Swift 5 / SwiftUI Native
Simple, with completion call-back and native SwiftUI #Binding
import SwiftUI
struct ShareSheet: UIViewControllerRepresentable {
typealias Callback = (_ activityType: UIActivity.ActivityType?, _ completed: Bool, _ returnedItems: [Any]?, _ error: Error?) -> Void
#Binding var isPresented: Bool
#Binding var activityItem: String
let applicationActivities: [UIActivity]? = nil
let excludedActivityTypes: [UIActivity.ActivityType]? = nil
let callback: Callback?
func makeUIViewController(context: Context) -> UIActivityViewController {
let controller = UIActivityViewController(
activityItems: [activityItem],
applicationActivities: applicationActivities)
controller.excludedActivityTypes = excludedActivityTypes
controller.completionWithItemsHandler = { (activityType, completed, returnedItems, error) in
callback?(activityType, completed, returnedItems, error)
isPresented = false
}
return controller
}
func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {
}
}
example usage:
ShareSheet(isPresented: $showShareSheet, activityItem: $sharingUrl, callback: { activityType, completed, returnedItems, error in
print("ShareSheet dismissed: \(activityType) \(completed) \(returnedItems) \(error)")
})

Display alert keeps logging me out after error message on xcode using swift

I'm creating an Instagram like app, and whenever I dismiss the display alert that is displayed when an image or text is missing, it logs me out and logs me back in. How can I make it so that when I click "ok", only the display alert goes away and doesn't log me out?
Here is my image posting view control code:
import UIKit
import Parse
import Foundation
class PostViewControllerPage1: UIViewController, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
func displayAlert(title:String, error:String) {
var alert = UIAlertController(title: title, message: error, preferredStyle: UIAlertControllerStyle.Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: { action in
self.dismissViewControllerAnimated(true, completion: nil)
}))
self.presentViewController(alert, animated: true, completion: nil)
}
var photoSelected:Bool = false
#IBOutlet weak var imageToPost: UIImageView!
#IBAction func chooseImage(sender: AnyObject) {
var image = UIImagePickerController()
image.delegate = self
image.sourceType = UIImagePickerControllerSourceType.PhotoLibrary
image.allowsEditing = false
self.presentViewController(image, animated: true, completion: nil)
}
func imagePickerController(picker: UIImagePickerController, didFinishPickingImage image: UIImage!, editingInfo: [NSObject : AnyObject]!) {
println("Image selected")
self.dismissViewControllerAnimated(true, completion: nil)
imageToPost.image = image
photoSelected = true
}
#IBOutlet weak var imageDescription: UITextField!
#IBAction func postImage(sender: AnyObject) {
var error = ""
if (photoSelected == false) {
error = "Please select an image to post"
} else if (imageDescription.text == "") {
error = "Please enter a message"
}
if (error != "") {
displayAlert("Cannot Post Image", error: error)
}
else {
var post = PFObject(className: "Post")
post["Title"] = imageDescription.text
post.saveInBackgroundWithBlock{(success: Bool, error: NSError?) -> Void in
if success == false {
self.displayAlert("Could Not Post Image", error: "Please try again later")
} else {
let imageData = UIImagePNGRepresentation(self.imageToPost.image)
let imageFile = PFFile(name: "image.png", data: imageData)
post["imageFile"] = imageFile
post.saveInBackgroundWithBlock{(success: Bool, error: NSError?) -> Void in
if success == false {
self.displayAlert("Could Not Post Image", error: "Please try again later")
} else {
println("posted successfully")
}
}
}
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
What am I doing wrong?
Here is my login code, because I feel like that might be a part of the issue:
import Foundation
import Parse
import UIKit
import Bolts
class LoginViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var loginStatusLabel: UILabel!
#IBOutlet weak var emailTextField: UITextField!
#IBOutlet weak var passwordTextField: UITextField!
#IBOutlet weak var loginButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
emailTextField.delegate = self
passwordTextField.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func loginButtonPress(sender: AnyObject) {
login(emailTextField.text, password: passwordTextField.text)
}
func login(email: String, password: String)
{
PFUser.logInWithUsernameInBackground(email, password: password)
{
(user: PFUser?, error: NSError?) -> Void in
if user != nil
{
user!.fetchInBackground()
if user!.objectForKey("emailVerified") as! Bool
{
self.loginStatusLabel.text = "Success!"
println("successfulLogin")
self.performSegueWithIdentifier("login", sender: self)
}
else if !(user!.objectForKey("emailVerified") as! Bool)
{
self.loginStatusLabel.text = "Verify your email address!"
}
else // status is "missing"
{
//TODO: Handle this error better
self.loginStatusLabel.text = "Verification status: Missing"
}
}
else
{
if let errorField = error!.userInfo
{
self.loginStatusLabel.text = (errorField["error"] as! NSString) as String
}
else
{
// No userInfo dictionary present
// Help from http://stackoverflow.com/questions/25381338/nsobject-anyobject-does-not-have-a-member-named-subscript-error-in-xcode
}
}
}
}
override func viewDidAppear(animated: Bool) {
if PFUser.currentUser() != nil {
self.performSegueWithIdentifier("login", sender: self)
}
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true;
}
override func viewWillAppear(animated: Bool) {
self.navigationController?.navigationBarHidden = true
}
override func viewWillDisappear(animated: Bool) {
self.navigationController?.navigationBarHidden = false
}
}
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: { action in
self.dismissViewControllerAnimated(true, completion: nil)
}))
self.dismissViewControllerAnimated in this line will dismiss view controller that show alert and not an alert itself. So all you have to do is comment or delete this line of code

Resources