Подтвердить что ты не робот

В iOS, как создать кнопку, которая всегда находится поверх всех других контроллеров?

Независимо от того, представлены ли модалы или пользователь выполняет любой тип segue.

Есть ли способ держать кнопку "всегда сверху" (не в верхней части экрана) во всем приложении?

Есть ли способ сделать эту кнопку перетаскиваемой и съемной на экран?

В качестве примера такой кнопки я смотрю на Apple Assistant Touch.

4b9b3361

Ответ 1

Вы можете сделать это, создав свой собственный подкласс UIWindow, а затем создав экземпляр этого подкласса. Вам нужно сделать три вещи в окне:

  • Установите windowLevel на очень высокое число, например CGFloat.max. Он зажат (с iOS 9.2) до 10000000, но вы также можете установить его на максимально возможное значение.

  • Установите цвет фона окна на нуль, чтобы сделать его прозрачным.

  • Переопределить метод pointInside(_:withEvent:) окна, чтобы вернуть true только для точек в кнопке. Это заставит окно принимать только штрихи, которые нажимают на кнопку, и все другие касания будут переданы другим окнам.

Затем создайте для окна контроллер корневого представления. Создайте кнопку и добавьте ее в иерархию представлений и расскажите об этом окне, чтобы окно могло использовать ее в pointInside(_:withEvent:).

Вот еще одна вещь. Оказывается, экранная клавиатура также использует самый высокий уровень окна, и поскольку она может появиться на экране после вашего окна, она будет поверх вашего окна. Вы можете исправить это, наблюдая за UIKeyboardDidShowNotification и перезагружая окно windowLevel, когда это произойдет (поскольку это документировано, чтобы поместить ваше окно поверх всех окон на одном уровне).

Вот демо. Я начну с контроллера представления, который я собираюсь использовать в окне.

import UIKit

class FloatingButtonController: UIViewController {

Здесь переменная экземпляра кнопки. Тот, кто создает FloatingButtonController, может получить доступ к кнопке, чтобы добавить к ней цель/действие. Я продемонстрирую это позже.

    private(set) var button: UIButton!

Вы должны "реализовать" этот инициализатор, но я не буду использовать этот класс в раскадровке.

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }

Здесь действительный инициализатор.

    init() {
        super.init(nibName: nil, bundle: nil)
        window.windowLevel = CGFloat.max
        window.hidden = false
        window.rootViewController = self

Настройка window.hidden = false выводит его на экран (когда фиксируется текущий CATransaction). Мне также нужно следить за появлением клавиатуры:

        NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardDidShowNotification, object: nil)
    }

Мне нужна ссылка на мое окно, которое будет экземпляром пользовательского класса:

    private let window = FloatingButtonWindow()

Я создам свою иерархию представления в коде, чтобы сохранить этот ответ автономным:

    override func loadView() {
        let view = UIView()
        let button = UIButton(type: .Custom)
        button.setTitle("Floating", forState: .Normal)
        button.setTitleColor(UIColor.greenColor(), forState: .Normal)
        button.backgroundColor = UIColor.whiteColor()
        button.layer.shadowColor = UIColor.blackColor().CGColor
        button.layer.shadowRadius = 3
        button.layer.shadowOpacity = 0.8
        button.layer.shadowOffset = CGSize.zero
        button.sizeToFit()
        button.frame = CGRect(origin: CGPointMake(10, 10), size: button.bounds.size)
        button.autoresizingMask = []
        view.addSubview(button)
        self.view = view
        self.button = button
        window.button = button

Ничего особенного здесь не происходит. Я просто создаю свой корневой вид и вставляю в него кнопку.

Чтобы пользователь мог перетащить кнопку вокруг, я добавлю кнопку распознавания жесты на кнопку:

        let panner = UIPanGestureRecognizer(target: self, action: "panDidFire:")
        button.addGestureRecognizer(panner)
    }

Окно будет выкладывать свои подпункты, когда оно впервые появится, и когда оно будет изменено (в частности, из-за поворота интерфейса), поэтому я хочу изменить положение кнопки в это время:

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        snapButtonToSocket()
    }

(Подробнее о snapButtonToSocket в ближайшее время.)

Чтобы обрабатывать перетаскивание кнопки, я использую распознаватель жесты панорамирования стандартным образом:

    func panDidFire(panner: UIPanGestureRecognizer) {
        let offset = panner.translationInView(view)
        panner.setTranslation(CGPoint.zero, inView: view)
        var center = button.center
        center.x += offset.x
        center.y += offset.y
        button.center = center

Вы попросили "привязку", поэтому, если панорама завершается или отменяется, я привяжу кнопку к одному из фиксированного числа позиций, которые я называю "сокетами":

        if panner.state == .Ended || panner.state == .Cancelled {
            UIView.animateWithDuration(0.3) {
                self.snapButtonToSocket()
            }
        }
    }

Я обрабатываю уведомление клавиатуры, сбросив window.windowLevel:

    func keyboardDidShow(note: NSNotification) {
        window.windowLevel = 0
        window.windowLevel = CGFloat.max
    }

Чтобы привязать кнопку к гнезду, я найду ближайший сокет в положение кнопки и переместите туда кнопку. Обратите внимание, что это не обязательно то, что вы хотите на вращение интерфейса, но я оставлю более совершенное решение в качестве упражнения для читателя. Во всяком случае, он держит кнопку на экране после поворота.

    private func snapButtonToSocket() {
        var bestSocket = CGPoint.zero
        var distanceToBestSocket = CGFloat.infinity
        let center = button.center
        for socket in sockets {
            let distance = hypot(center.x - socket.x, center.y - socket.y)
            if distance < distanceToBestSocket {
                distanceToBestSocket = distance
                bestSocket = socket
            }
        }
        button.center = bestSocket
    }

Я помещаю сокет в каждый угол экрана, а один в середине для демонстрационных целей:

    private var sockets: [CGPoint] {
        let buttonSize = button.bounds.size
        let rect = view.bounds.insetBy(dx: 4 + buttonSize.width / 2, dy: 4 + buttonSize.height / 2)
        let sockets: [CGPoint] = [
            CGPointMake(rect.minX, rect.minY),
            CGPointMake(rect.minX, rect.maxY),
            CGPointMake(rect.maxX, rect.minY),
            CGPointMake(rect.maxX, rect.maxY),
            CGPointMake(rect.midX, rect.midY)
        ]
        return sockets
    }

}

Наконец, пользовательский подкласс UIWindow:

private class FloatingButtonWindow: UIWindow {

    var button: UIButton?

    init() {
        super.init(frame: UIScreen.mainScreen().bounds)
        backgroundColor = nil
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

Как я уже упоминал, мне нужно переопределить pointInside(_:withEvent:), чтобы окно игнорировало касания вне кнопки:

    private override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
        guard let button = button else { return false }
        let buttonPoint = convertPoint(point, toView: button)
        return button.pointInside(buttonPoint, withEvent: event)
    }

}

Теперь, как вы используете эту вещь? Я загрузил образец проекта Apple AdaptivePhotos и добавил мой файл FloatingButtonController.swift в цель AdaptiveCode. Я добавил свойство AppDelegate:

var floatingButtonController: FloatingButtonController?

Затем я добавил код в конец application(_:didFinishLaunchingWithOptions:), чтобы создать FloatingButtonController:

    floatingButtonController = FloatingButtonController()
    floatingButtonController?.button.addTarget(self, action: "floatingButtonWasTapped", forControlEvents: .TouchUpInside)

Эти строки идут прямо перед return true в конце функции. Мне также нужно написать метод действия для кнопки:

func floatingButtonWasTapped() {
    let alert = UIAlertController(title: "Warning", message: "Don't do that!", preferredStyle: .Alert)
    let action = UIAlertAction(title: "Sorry…", style: .Default, handler: nil)
    alert.addAction(action)
    window?.rootViewController?.presentViewController(alert, animated: true, completion: nil)
}

Это все, что я должен был сделать. Но я сделал еще одну вещь для демонстрационных целей: в AboutViewController я изменил label на UITextView, чтобы у меня был способ открыть клавиатуру.

Вот что выглядит кнопка:

перетаскивание кнопки

Здесь эффект нажатия кнопки. Обратите внимание, что кнопка плавает над предупреждением:

нажатие кнопки

Вот что происходит, когда я поднимаю клавиатуру:

keyboard

Он обрабатывает вращение? Вы ставите:

rotation

Ну, обработка вращения не идеальна, потому что ближайший к ней сокет после поворота может быть не таким же "логическим", как перед вращением. Вы можете исправить это, отслеживая, какой сокет была нажата последняя кнопка, и особенно обрабатывать вращение (путем обнаружения изменения размера).

Я положил весь FloatingViewController.swift в этот смысл для вашего удобства.

Ответ 2

В Swift 3:

import UIKit

private class FloatingButtonWindow: UIWindow {
    var button: UIButton?

    var floatingButtonController: FloatingButtonController?

    init() {
        super.init(frame: UIScreen.main.bounds)
        backgroundColor = nil
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    fileprivate override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        guard let button = button else { return false }
        let buttonPoint = convert(point, to: button)
        return button.point(inside: buttonPoint, with: event)
    }
}

class FloatingButtonController: UIViewController {
    private(set) var button: UIButton!

    private let window = FloatingButtonWindow()

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }

    init() {
        super.init(nibName: nil, bundle: nil)
        window.windowLevel = CGFloat.greatestFiniteMagnitude
        window.isHidden = false
        window.rootViewController = self
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
    }

    func keyboardDidShow(note: NSNotification) {
        window.windowLevel = 0
        window.windowLevel = CGFloat.greatestFiniteMagnitude
    }

    override func loadView() {
        let view = UIView()
        let button = UIButton(type: .custom)
        button.setTitle("Floating", for: .normal)
        button.setTitleColor(UIColor.green, for: .normal)
        button.backgroundColor = UIColor.white
        button.layer.shadowColor = UIColor.black.cgColor
        button.layer.shadowRadius = 3
        button.layer.shadowOpacity = 0.8
        button.layer.shadowOffset = CGSize.zero
        button.sizeToFit()
        button.frame = CGRect(origin: CGPoint(x: 10, y: 10), size: button.bounds.size)
        button.autoresizingMask = []
        view.addSubview(button)
        self.view = view
        self.button = button
        window.button = button


        let panner = UIPanGestureRecognizer(target: self, action: #selector(panDidFire))
        button.addGestureRecognizer(panner)
    }

    func panDidFire(panner: UIPanGestureRecognizer) {
        let offset = panner.translation(in: view)
        panner.setTranslation(CGPoint.zero, in: view)
        var center = button.center
        center.x += offset.x
        center.y += offset.y
        button.center = center

        if panner.state == .ended || panner.state == .cancelled {
            UIView.animate(withDuration: 0.3) {
                self.snapButtonToSocket()
            }
        }
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        snapButtonToSocket()
    }

    private var sockets: [CGPoint] {
        let buttonSize = button.bounds.size
        let rect = view.bounds.insetBy(dx: 4 + buttonSize.width / 2, dy: 4 + buttonSize.height / 2)
        let sockets: [CGPoint] = [
            CGPoint(x: rect.minX, y: rect.minY),
            CGPoint(x: rect.minX, y: rect.maxY),
            CGPoint(x: rect.maxX, y: rect.minY),
            CGPoint(x: rect.maxX, y: rect.maxY),
            CGPoint(x: rect.midX, y: rect.midY)
        ]
        return sockets
    }

    private func snapButtonToSocket() {
        var bestSocket = CGPoint.zero
        var distanceToBestSocket = CGFloat.infinity
        let center = button.center
        for socket in sockets {
            let distance = hypot(center.x - socket.x, center.y - socket.y)
            if distance < distanceToBestSocket {
                distanceToBestSocket = distance
                bestSocket = socket
            }
        }
        button.center = bestSocket
    }
}

И в AppDelegate:

    var floatingButtonController: FloatingButtonController?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.

     floatingButtonController = FloatingButtonController()
     floatingButtonController?.button.addTarget(self, action: #selector(AppDelegate.floatingButtonWasTapped), for: .touchUpInside)


    return true
}

func floatingButtonWasTapped() {
    let alert = UIAlertController(title: "Warning", message: "Don't do that!", preferredStyle: .alert)
    let action = UIAlertAction(title: "Sorry…", style: .default, handler: nil)
    alert.addAction(action)
    window?.rootViewController?.present(alert, animated: true, completion: nil)
}

Ответ 3

В этом случае намерение Apple состояло бы в том, чтобы ваш контроллер корневого представления содержал кнопку и любую логику прикосновения, которую вы хотите, и что он также встраивается в качестве контроллера детского представления, который запускает остальную часть приложения. Вы не собираетесь напрямую разговаривать с UIWindow и, вероятно, столкнетесь с целым рядом проблем с такими вещами, как авторотация между версиями iOS.

Старый код может по-прежнему удалять представления модалов сверху с использованием устаревших классов, таких как UIAlertView. Apple явно переключилась на такие подходы, как UIAlertController, когда предмет, который покрыт, явно владеет тем, что покрывает его, чтобы обеспечить надлежащее сохранение контроллера. Вероятно, вы должны сделать одно и то же предположение и отказаться от использования компонентов, которые находятся на устаревшем коде.

Ответ 4

Swift 4.2 - расширение UIViewController

Модифицированная версия Rob Mayoff отвечает за локальные плавающие кнопки.

Расширение UIViewController может предоставить вам лучший контроль над плавающей кнопкой. Однако он будет добавлен к одному контроллеру представления, который вы вызываете addFloatingButton вместо всех контроллеров представления.

  • Добавление плавающей кнопки в конкретном контроллере представления позволяет добавить определенные действия контроллера представления к плавающей кнопке.
  • Он не заблокирует функцию topViewController, если вы будете часто ее использовать. (Приведено ниже)
  • Если вы используете плавающую кнопку для переключения поддержки чата для вашего приложения (например, Zendesk Chat API), метод расширения UIViewController предоставляет вам лучший контроль над навигацией между контроллерами представления.

Однако в этом вопросе это может быть не самой лучшей практикой, так как управление типом над плавающей кнопкой должно включать разные методы для разных целей.

Расширение UIViewController

import UIKit

extension UIViewController {
    private struct AssociatedKeys {
        static var floatingButton: UIButton?
    }

    var floatingButton: UIButton? {
        get {
            guard let value = objc_getAssociatedObject(self, &AssociatedKeys.floatingButton) as? UIButton else {return nil}
            return value
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.floatingButton, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    func addFloatingButton() {
        // Customize your own floating button UI
        let button = UIButton(type: .custom)
        let image = UIImage(named: "tab_livesupport_unselected")?.withRenderingMode(.alwaysTemplate)
        button.tintColor = .white
        button.setImage(image, for: .normal)
        button.backgroundColor = UIColor.obiletGreen
        button.layer.shadowColor = UIColor.black.cgColor
        button.layer.shadowRadius = 3
        button.layer.shadowOpacity = 0.12
        button.layer.shadowOffset = CGSize(width: 0, height: 1)
        button.sizeToFit()
        let buttonSize = CGSize(width: 60, height: 60)
        let rect = UIScreen.main.bounds.insetBy(dx: 4 + buttonSize.width / 2, dy: 4 + buttonSize.height / 2)
        button.frame = CGRect(origin: CGPoint(x: rect.maxX - 15, y: rect.maxY - 50), size: CGSize(width: 60, height: 60))
        // button.cornerRadius = 30 -> Will destroy your shadows, however you can still find workarounds for rounded shadow.
        button.autoresizingMask = []
        view.addSubview(button)
        floatingButton = button
        let panner = UIPanGestureRecognizer(target: self, action: #selector(panDidFire(panner:)))
        floatingButton?.addGestureRecognizer(panner)
        snapButtonToSocket()
    }

    @objc fileprivate func panDidFire(panner: UIPanGestureRecognizer) {
        guard let floatingButton = floatingButton else {return}
        let offset = panner.translation(in: view)
        panner.setTranslation(CGPoint.zero, in: view)
        var center = floatingButton.center
        center.x += offset.x
        center.y += offset.y
        floatingButton.center = center

        if panner.state == .ended || panner.state == .cancelled {
            UIView.animate(withDuration: 0.3) {
                self.snapButtonToSocket()
            }
        }
    }

    fileprivate func snapButtonToSocket() {
        guard let floatingButton = floatingButton else {return}
        var bestSocket = CGPoint.zero
        var distanceToBestSocket = CGFloat.infinity
        let center = floatingButton.center
        for socket in sockets {
            let distance = hypot(center.x - socket.x, center.y - socket.y)
            if distance < distanceToBestSocket {
                distanceToBestSocket = distance
                bestSocket = socket
            }
        }
        floatingButton.center = bestSocket
    }

    fileprivate var sockets: [CGPoint] {
        let buttonSize = floatingButton?.bounds.size ?? CGSize(width: 0, height: 0)
        let rect = view.bounds.insetBy(dx: 4 + buttonSize.width / 2, dy: 4 + buttonSize.height / 2)
        let sockets: [CGPoint] = [
            CGPoint(x: rect.minX + 15, y: rect.minY + 30),
            CGPoint(x: rect.minX + 15, y: rect.maxY - 50),
            CGPoint(x: rect.maxX - 15, y: rect.minY + 30),
            CGPoint(x: rect.maxX - 15, y: rect.maxY - 50)
        ]
        return sockets
    }
}

// Custom socket position to hold Y position and snap to horizontal edges.
// You can snap to any coordinate on screen by setting custom socket positions.
fileprivate var horizontalSockets: [CGPoint] {
        guard let floatingButton = floatingButton else {return []}
        let buttonSize = floatingButton.bounds.size
        let rect = view.bounds.insetBy(dx: 4 + buttonSize.width / 2, dy: 4 + buttonSize.height / 2)
        let y = min(rect.maxY - 50, max(rect.minY + 30, floatingButton.frame.minY + buttonSize.height / 2))
        let sockets: [CGPoint] = [
            CGPoint(x: rect.minX + 15, y: y),
            CGPoint(x: rect.maxX - 15, y: y)
        ]
        return sockets
    }

UIViewController Использование

Я предпочитаю добавлять плавающую кнопку после viewDidLoad (_ animated :). Может потребоваться вызвать метод acceptSubviewToFront(), если впоследствии другое подпредставление блокирует плавающую кнопку.

override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        addFloatingButton()
        floatingButton?.addTarget(self, action: #selector(floatingButtonPressed), for: .touchUpInside)
    }

    @objc func floatingButtonPressed(){
        print("Floating button tapped")
    }

UIApplication - контроллер верхнего вида

extension UIApplication{

    class func topViewController(controller: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
        if let navigationController = controller as? UINavigationController {
            return topViewController(controller: navigationController.visibleViewController)
        }
        if let tabController = controller as? UITabBarController {
            if let selected = tabController.selectedViewController {
                return topViewController(controller: selected)
            }
        }
        if let presented = controller?.presentedViewController {
            return topViewController(controller: presented)
        }
        return controller
    }
}

Ответ 5

Вы можете использовать Paramount, который в основном использует другой UIWindow для отображения вашей кнопки

Manager.action = {
  print("action touched")
}

Manager.show()

Ответ 6

Вы можете использовать свойство zPosition слоя вида (это объект CALayer) для изменения z-индекса представления, также вам нужно добавить эту кнопку или ваше представление в качестве подзаголовка вашего window не является подвидным вашим любым другим контроллерам view, его значение по умолчанию равно 0. Но вы можете изменить его следующим образом:

yourButton.layer.zPosition = 1;

Вам нужно импортировать среду QuartzCore для доступа к этому слою. Просто добавьте эту строку кода вверху вашего файла реализации.

#import "QuartzCore/QuartzCore.h"

Надеюсь, что это поможет.

Ответ 7

Представьте это:

public static var topMostVC: UIViewController? {
    var presentedVC = UIApplication.sharedApplication().keyWindow?.rootViewController
    while let pVC = presentedVC?.presentedViewController {
        presentedVC = pVC
    }

    if presentedVC == nil {
        print("EZSwiftExtensions Error: You don't have any views set. You may be calling them in viewDidLoad. Try viewDidAppear instead.")
    }
    return presentedVC
}

topMostVC?.presentViewController(myAlertController, animated: true, completion: nil)

//or

let myView = UIView()
topMostVC?.view?.addSubview(myView)

Они включены как стандартная функция в моем проекте: https://github.com/goktugyil/EZSwiftExtensions

Ответ 8

Добавить кнопку в UIWindow, она будет плавать во всех представлениях в приложении

Ответ 9

Независимо от того, представлены ли модалы или пользователь выполняет любой тип segue.

Простой ответ: да, есть способ. Вы программист, поэтому вы почти полностью контролируете, что пользователь может делать с вашим приложением. Может быть несколько вещей, которые вы не можете контролировать, например, что происходит, когда устройство получает телефонный звонок, помещает ваше приложение в фоновом режиме и т.д., Но это действительно внешние факторы, которые никто не мог бы считать частью вашего приложения.

Более честный ответ: уровень усилий, необходимых для того, чтобы это произошло, может быть настолько большим, чтобы быть запретительным. Вот несколько возможных подходов:

  • Вы можете отказаться от большинства инструментов, которые UIKit дает вам и катит свои собственные модальные диалоги, переходы и т.д. Если вы пишете все сами, вы можете заставить его работать, как вам нравится. Но это большая работа.

  • Создайте единый контроллер представлений, который содержит все остальные контроллеры представлений в вашем приложении, выполняя сами переходы, а не полагаясь на segues. Этот контроллер просмотра может управлять кнопкой и держать ее поверх всех других представлений в иерархии представлений.

  • Создайте общий суперконтроль контроллера представления, который управляет кнопкой, а затем выведет из этого все остальные контроллеры представлений, чтобы каждый контроллер представления в вашем приложении имел свою собственную кнопку, но каждая кнопка выглядит одинаково и работает то же самое, что касается пользователя, есть действительно одна кнопка.

Я думаю, что лучший ответ: нет, действительно нет хорошего способа сделать это в пределах UIKit; вам лучше подумать о том, что вы пытаетесь выполнить, и искать способы добиться этого, работая с каркасом, а не против него.

Есть ли способ сделать эту кнопку перетаскиваемой и съемной на экран?

Опять же, вы программист, поэтому можете делать то, что вам нравится. В этом случае задача намного проще. Что такое кнопка, но представление, которое вызывает действие при нажатии? Даже со стандартным классом UIButton вы можете подключить распознаватель жестов, который позволит вам перетащить кнопку вокруг, а так как вы пишете код, вы можете определить, где эта кнопка может появиться на экране, поэтому привязка очень возможна.