2016-01-13 9 views
24

Egal, ob Modale präsentiert werden oder der Benutzer irgendeine Art von Überleitung ausführt.Wie erstelle ich in iOS eine Schaltfläche, die immer über allen anderen View-Controllern steht?

Gibt es eine Möglichkeit, die Schaltfläche "immer oben" (nicht oben auf dem Bildschirm) in der gesamten App zu halten?

Gibt es eine Möglichkeit, diese Schaltfläche auf dem Bildschirm ziehbar und feststellbar zu machen?

Ich sehe Apples eigene Assistive Touch als Beispiel für einen solchen Knopf.

+0

Fragen Sie nach der Schaltfläche zu erhöhen? –

+0

Wessen Modale? Woher kommen diese Modale? – Tommy

+0

Die Modale könnten von jedem ViewController kommen, der sie anruft. – TIMEX

Antwort

58

Sie können dies tun, indem Sie Ihre eigene Unterklasse von UIWindow erstellen und dann eine Schaffung Instanz dieser Unterklasse.

  1. Legen Sie seine windowLevel auf eine sehr hohe Zahl, wie CGFloat.max: Sie werden drei Dinge zum Fenster tun müssen. Es ist (wie von iOS 9.2) auf 10000000 geklemmt, aber Sie können es auch auf den höchstmöglichen Wert setzen.

  2. Setzen Sie die Hintergrundfarbe des Fensters auf Null, um es transparent zu machen.

  3. Überschreiben Sie die Methode pointInside(_:withEvent:) des Fensters, um nur für die Punkte in der Schaltfläche true zurückzugeben. Dadurch werden nur Berührungen akzeptiert, die die Schaltfläche berühren, und alle anderen Berührungen werden an andere Fenster weitergegeben.

dann eine Wurzel-View-Controller für das Fenster erstellen. Erstellen Sie die Schaltfläche und fügen Sie sie zur Ansichtshierarchie hinzu, und teilen Sie das Fenster darüber mit, damit das Fenster sie in pointInside(_:withEvent:) verwenden kann.

Es gibt eine letzte Sache zu tun. Es stellt sich heraus, dass die Bildschirmtastatur auch die höchste Fensterebene verwendet, und da sie nach dem Fenster auf dem Bildschirm erscheint, wird sie über Ihrem Fenster angezeigt. Sie können das beheben, indem Sie UIKeyboardDidShowNotification beobachten und die windowLevel Ihres Fensters zurücksetzen, wenn dies geschieht (weil dies dokumentiert ist, um Ihr Fenster über alle Fenster auf derselben Ebene zu setzen).

Hier ist eine Demo.Ich beginne mit dem View-Controller, den ich im Fenster verwenden werde.

import UIKit 

class FloatingButtonController: UIViewController { 

Hier ist die Schaltfläche Instanzvariable. Wer eine FloatingButtonController erstellt, kann auf die Schaltfläche zugreifen, um ein Ziel/eine Aktion hinzuzufügen. Ich werde das später demonstrieren.

private(set) var button: UIButton! 

Sie müssen diesen Initialisierer "implementieren", aber ich werde diese Klasse nicht in einem Storyboard verwenden.

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

Hier ist der echte Initialisierer.

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

window.hidden = false Einstellung wird es auf dem Bildschirm (wenn der Strom CATransaction Commits). Ich muss auch sehen für die Tastatur erscheinen:

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

Ich brauche einen Verweis auf meinem Fenster, das eine Instanz einer benutzerdefinierten Klasse sein wird:

private let window = FloatingButtonWindow() 

Ich werde meine Ansicht Hierarchie erstellen In Code, um diese Antwort in sich geschlossen zu halten:

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 

Nichts Besonderes dort los. Ich erstelle gerade meine Root-Ansicht und lege eine Schaltfläche darin ab.

der Benutzer zu ermöglichen, den Knopf um zu ziehen, werde ich einen pan Gestenerkenner der Schaltfläche hinzu:

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

Das Fenster wird seine Subviews legen, wenn es zuerst erscheint, und wenn es geändert wird (insbesondere wegen der Schnittstelle Rotation), so möchte ich den Knopf zu diesen Zeiten neu zu positionieren:

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

(Mehr auf snapButtonToSocket kurz.)

um han DLE den Knopf ziehen, verwende ich die Pfanne Gestenerkenner auf die übliche Weise:

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 

Sie fragte nach „schnappen“, so dass, wenn die Pfanne oder abgebrochen wird am Ende, ich die Taste, um eine von einem festen Snap werden Anzahl der Positionen, die ich „Steckdosen“ nennen:

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

ich behandle die Tastatur Benachrichtigung durch window.windowLevel Zurücksetzen:

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

um Snap auf die Schaltfläche an eine Steckdose, finde ich das nächste Steckdose an der Position des Knopfes und bewegen Sie den Knopf dort. Beachten Sie, dass dies nicht unbedingt das ist, was Sie bei einer Schnittstellendrehung wünschen, aber ich werde eine bessere Lösung als Übung für den Leser hinterlassen. Jedenfalls hält es nach einer Drehung den Knopf auf dem Bildschirm.

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 
    } 

habe ich eine Steckdose in jeder Ecke des Bildschirms, und ein in der Mitte zu Demonstrationszwecken:

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 
    } 

} 

Schließlich ist die benutzerdefinierte UIWindow Unterklasse:

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") 
    } 

Wie ich erwähnt, muss ich pointInside(_:withEvent:) überschreiben, so dass das Fenster Berührungen außerhalb der Schaltfläche ignoriert:

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) 
    } 

} 

Nun, wie benutzt du dieses Ding? Ich habe Apple's AdaptivePhotos sample project heruntergeladen und meine FloatingButtonController.swift Datei zum AdaptiveCode Ziel hinzugefügt. Ich habe eine Immobilie zu AppDelegate:

var floatingButtonController: FloatingButtonController? 

Dann habe ich Code bis zum Ende des application(_:didFinishLaunchingWithOptions:) erstellen FloatingButtonController:

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

Diese Linien fahren direkt vor dem return true am Ende der Funktion. Ich muss auch die Aktionsmethode für die Schaltfläche schreiben:

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) 
} 

Das war alles, was ich tun musste. Aber ich machte noch eine Sache zu Demonstrationszwecken: In AboutViewController habe ich label in eine UITextView geändert, damit ich eine Möglichkeit habe, die Tastatur aufzurufen.

Hier ist, was die Schaltfläche wie folgt aussieht:

dragging the button

Hier ist der Effekt auf die Schaltfläche des Klopfens. Beachten Sie, dass die Taste über dem Alarm schwimmt:

clicking the button

Hier ist, was passiert, wenn ich die Tastatur bringen:

keyboard

es Drehgriff Hat? Sie wetten:

rotation

Nun, die rotations Handhabung ist nicht perfekt, weil der Sockel am nächsten ist die Taste nach einer Drehung nicht die gleichen „logischen“ socket wie vor der Drehung sein kann. Sie können dies beheben, indem Sie verfolgen, an welchem ​​Sockel die Schaltfläche zuletzt geklickt wurde, und eine Rotation speziell behandeln (indem Sie die Größenänderung erkennen).

Ich habe die gesamte FloatingViewController.swift in this gist für Ihre Bequemlichkeit.

+2

, ein Blick auf das [* Ansicht Programmierhandbuch für iOS *] (https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/CreatingWindows/CreatingWindows.html#//apple_ref/doc/uid/TP40009503-CH4-SW13) hat mich beobachten Sie wahrscheinlich wollen auch 'UIWindowDidBecomeVisibleNotification' und' window.windowLevel' zurückgesetzt denken gemacht, wenn Sie einen bekommen. –

+3

Große Erklärung. Kannst du die Objective-C-Version auch @rob bereitstellen? – pkc456

+1

Direkt um Apple zu zitieren: "Um den Inhalt Ihrer App zu ändern, können Sie die Hauptansicht des Fensters ändern; * Sie erstellen kein neues Fenster. *"; "Die meisten iOS-Anwendungen erstellen und verwenden nur ein Fenster während ihrer Lebensdauer. ... Wenn jedoch eine Anwendung die Verwendung einer externen Anzeige für Video-Out unterstützt, kann es ein zusätzliches Fenster erstellen ... Alle anderen Fenster werden normalerweise von erstellt System"; etc, usw. Wie jeder Versuch eines Drittanbieters bei einem UIAlertView-Workalike (bevor Apple entschieden hat, dass es sich bei dem Modeln auf Fensterebene um ein unterbrochenes Muster handelt), sollte dieser Ansatz regelmäßig unterbrochen werden. – Tommy

-2

Egal ob modals präsentiert werden oder der Benutzer irgendeine Art von Überleitung durchführt.

Die einfache Antwort ist: ja, es gibt einen Weg. Sie sind der Programmierer, sodass Sie fast vollständig kontrollieren können, was ein Benutzer mit Ihrer App tun kann. Es kann einige Dinge geben, die Sie nicht kontrollieren können, z. B. was passiert, wenn das Gerät einen Anruf erhält, Ihre App in den Hintergrund stellt, usw., aber dies sind wirklich externe Faktoren, die niemand als Teil Ihrer Anwendung betrachten würde .

Die ehrlichere Antwort ist: das Niveau der Anstrengung, die erforderlich ist, um das geschehen zu lassen, kann so groß sein, prohibitiv zu sein. Hier sind ein paar mögliche Ansätze:

  • Sie die meisten Tools verzichten könnte, die UIKit gibt Ihnen und Ihren eigenen modale Dialoge rollen, Übergänge, etc. Wenn Sie alles selbst schreiben, können Sie es sich arbeiten jedoch mögen. Aber das ist ein Los der Arbeit.

  • Erstellen Sie einen Einzelansicht-Controller, der alle anderen View-Controller in Ihrer Anwendung enthält. Führen Sie die Übergänge selbst aus, anstatt sich auf Segmente zu verlassen. Dieser Ansichtscontroller könnte die Schaltfläche verwalten und sie über allen anderen Ansichten in der Ansichtshierarchie halten.

  • Erstellen Sie eine allgemeine View-Controller-Superklasse, die eine Schaltfläche verwaltet, und leiten Sie dann alle anderen View-Controller ab, sodass jeder View-Controller in Ihrer App über eine eigene Schaltfläche verfügt, aber jede Schaltfläche gleich aussieht und gleich funktioniert , so weit der Benutzer betroffen ist, gibt es wirklich nur einen Knopf.

Ich denke, die beste Antwort ist: nein, es ist wirklich kein guter Weg, dass der UIKit innerhalb der Grenzen zu tun; Sie sollten besser darüber nachdenken, was Sie erreichen möchten, und nach Möglichkeiten suchen, dies zu erreichen, während Sie mit dem Framework arbeiten anstatt dagegen.

Gibt es eine Möglichkeit, diese Schaltfläche ziehbar und auf dem Bildschirm feststellbar?

Wieder sind Sie der Programmierer, also können Sie tun, was Sie wollen. In diesem Fall ist die Aufgabe jedoch viel einfacher. Was ist eine Schaltfläche, sondern eine Ansicht, die beim Antippen eine Aktion auslöst? Selbst mit der Standardklasse UIButton können Sie eine Gestenerkennung anschließen, mit der Sie die Schaltfläche verschieben können. Da Sie den Code schreiben, können Sie festlegen, wo diese Schaltfläche auf dem Bildschirm angezeigt werden soll, sodass ein Fangen sehr gut möglich ist.

3

Apples Absicht in diesem Fall wäre, dass Ihr root-View-Controller die Schaltfläche und die gewünschte Touch-Logik enthält, und dass es auch als Kind View-Controller einbetten, was auch immer den Rest der App läuft. Sie sind nicht dazu gedacht, direkt mit UIWindow zu sprechen, und werden wahrscheinlich eine ganze Reihe von Problemen mit Dingen wie Autorotation über Versionen von iOS treffen, wenn Sie dies tun.

Der alte Code kann immer noch die Modale oben mit veralteten Klassen wie UIAlertView präsentieren. Apple hat explizit zu Ansätzen wie UIAlertController gewechselt, wo das Ding, das abgedeckt wird, explizit das Ding besitzt, das es abdeckt, um eine angemessene Kontrolle des View-Controllers zu ermöglichen. Sie sollten wahrscheinlich die gleiche Annahme treffen und es ablehnen, Komponenten zu verwenden, die auf veraltetem Code basieren.

+0

Ich habe eine leere ViewController mit einer Containeransicht, die den gesamten Bildschirm füllt. Die Containeransicht enthält einen TabBarViewController (was meine Hauptanwendung ist). Ich habe eine Ansicht im leerenViewController erstellt, und es bleibt. Wenn es jedoch ein Modal gibt, deckt es die Ansicht ab. – TIMEX

0

können Sie verwenden, um die zPosition Eigenschaft der Schicht Sicht (es ist ein CALayer Objekt) den Z-Index der Ansicht zu ändern, auch müssen Sie diese Schaltfläche oder Ihre Ansicht als Subview des Fensters hinzufügen nicht als Subview Ihre andere Viewcontrollers, dessen Standardwert ist 0. Aber man kann es wie folgt ändern:

yourButton.layer.zPosition = 1; 

Sie müssen den Quartz Rahmen importieren, um die Schicht zuzugreifen. Fügen Sie diese Codezeile am Anfang Ihrer Implementierungsdatei hinzu.

#import "QuartzCore/QuartzCore.h" 

Hoffe, das hilft.

0

Präsens es dazu:

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) 

Diese werden als Standardfunktion in einem Projekt von mir enthalten: https://github.com/goktugyil/EZSwiftExtensions

+1

Wenn Sie Links zu Ihren eigenen GitHub-Projekten posten, sollten Sie die Leute wissen lassen, dass es Ihre ist. Andernfalls besteht die Gefahr, dass Ihre Beiträge als Spam markiert werden. – DavidG

+0

Ich habe diese Datei vergessen, sie können sie bearbeiten, wenn Sie Beiträge wie diese sehen. Eigentlich – Esqarrouth

0

Schaltfläche Hinzufügen auf UIWindow, schwimmt es auf allen Ansichten in App

2

Sie Paramount verwenden können, die im Grunde eine andere UIWindow verwendet Ihre Schaltfläche

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

Manager.show() 

0

In Swift anzuzeigen 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 
    } 
} 

Und in 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) 
} 
Verwandte Themen