Swift 앱에 여러 개의 뷰 컨트롤러가 있고 그들 사이에 데이터를 전달할 수 있기를 원한다고 가정 해 보겠습니다. 뷰 컨트롤러 스택에서 여러 수준 아래에있는 경우 데이터를 다른 뷰 컨트롤러로 어떻게 전달합니까? 아니면 탭 모음보기 컨트롤러의 탭간에?
(참고로,이 질문은 "벨소리"입니다.) 질문이 너무 많아서 주제에 대한 자습서를 작성하기로 결정했습니다. 아래 내 대답을 참조하십시오.
Swift 앱에 여러 개의 뷰 컨트롤러가 있고 그들 사이에 데이터를 전달할 수 있기를 원한다고 가정 해 보겠습니다. 뷰 컨트롤러 스택에서 여러 수준 아래에있는 경우 데이터를 다른 뷰 컨트롤러로 어떻게 전달합니까? 아니면 탭 모음보기 컨트롤러의 탭간에?
(참고로,이 질문은 "벨소리"입니다.) 질문이 너무 많아서 주제에 대한 자습서를 작성하기로 결정했습니다. 아래 내 대답을 참조하십시오.
답변:
귀하의 질문은 매우 광범위합니다. 모든 시나리오에 대해 하나의 간단한 포괄 솔루션이 있다고 제안하는 것은 약간 순진합니다. 따라서 이러한 시나리오 중 일부를 살펴 보겠습니다.
내 경험상 Stack Overflow에서 가장 일반적인 시나리오는 하나의 뷰 컨트롤러에서 다음 컨트롤러로 정보를 전달하는 것입니다.
스토리 보드를 사용하는 경우 첫 번째 뷰 컨트롤러는을 재정의 할 수 있습니다 prepareForSegue. UIStoryboardSegue개체는이 메소드를 호출 할 때 전달하고, 우리 목적지보기 컨트롤러에 대한 참조를 포함한다. 여기에서 전달하려는 값을 설정할 수 있습니다.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "MySegueID" {
if let destination = segue.destination as? SecondController {
destination.myInformation = self.myInformation
}
}
}
또는 스토리 보드를 사용하지 않는 경우 펜촉에서 뷰 컨트롤러를로드합니다. 그러면 코드가 약간 더 간단합니다.
func showNextController() {
let destination = SecondController(nibName: "SecondController", bundle: nil)
destination.myInformation = self.myInformation
show(destination, sender: self)
}
두 경우 모두 myInformation하나의 뷰 컨트롤러에서 다음 뷰 컨트롤러로 전달해야하는 데이터를 보유하는 각 뷰 컨트롤러의 속성입니다. 분명히 각 컨트롤러에서 같은 이름을 가질 필요는 없습니다.
또한 UITabBarController.
이 경우 실제로 잠재적으로 더 간단합니다.
먼저의 하위 클래스를 만들고 UITabBarController다양한 탭간에 공유하려는 정보에 대한 속성을 지정합니다.
class MyCustomTabController: UITabBarController {
var myInformation: [String: AnyObject]?
}
이제 스토리 보드에서 앱을 빌드하는 경우 탭 막대 컨트롤러의 클래스를 기본값 UITabBarController에서 MyCustomTabController. 스토리 보드를 사용하지 않는 경우 기본 UITabBarController클래스가 아닌이 사용자 지정 클래스의 인스턴스를 인스턴스화 하고 여기에 뷰 컨트롤러를 추가합니다.
이제 탭 막대 컨트롤러 내의 모든 뷰 컨트롤러는 다음과 같이이 속성에 액세스 할 수 있습니다.
if let tbc = self.tabBarController as? MyCustomTabController {
// do something with tbc.myInformation
}
그리고 UINavigationController같은 방식 으로 서브 클래 싱 함으로써 전체 탐색 스택에서 데이터를 공유하는 동일한 접근 방식을 취할 수 있습니다.
if let nc = self.navigationController as? MyCustomNavController {
// do something with nc.myInformation
}
몇 가지 다른 시나리오가 있습니다. 이 답변이 모든 것을 포함하지는 않습니다.
prepareForSegue. 이 매우 간단한 관찰이 여기에있는 다른 답변과 여담 사이에서 잃어버린 것은 너무 나쁩니다.
prepareForSegue하거나 다른 방식으로 전달한 다음 초보자가 이러한 상황이 작동하지 않는 시나리오를 표시 할 때 간단하게 괜찮아 져야하며 그런 다음 이러한보다 글로벌 한 접근 방식에 대해 가르쳐야합니다.
이 질문은 항상 떠 오릅니다.
한 가지 제안은 데이터 컨테이너 싱글 톤을 만드는 것입니다. 애플리케이션 수명에서 한 번만 생성되고 앱 수명 동안 지속되는 개체입니다.
이 접근 방식은 앱의 여러 클래스에서 사용 / 수정해야하는 글로벌 앱 데이터가있는 상황에 적합합니다.
뷰 컨트롤러간에 단방향 또는 양방향 링크를 설정하는 것과 같은 다른 접근 방식은 뷰 컨트롤러간에 직접 정보 / 메시지를 전달하는 상황에 더 적합합니다.
(다른 대안은 아래 nhgrif의 답변을 참조하십시오.)
데이터 컨테이너 싱글 톤을 사용하면 싱글 톤에 대한 참조를 저장하는 속성을 클래스에 추가 한 다음 액세스가 필요할 때마다 해당 속성을 사용합니다.
싱글 톤을 설정하여 콘텐츠를 디스크에 저장하여 실행 사이에 앱 상태가 유지되도록 할 수 있습니다.
이 작업을 수행하는 방법을 보여주는 GitHub에 데모 프로젝트를 만들었습니다. 여기 링크가 있습니다:
GitHub의 SwiftDataContainerSingleton 프로젝트 다음은 해당 프로젝트의 README입니다.
데이터 컨테이너 싱글 톤을 사용하여 애플리케이션 상태를 저장하고 객체간에 공유하는 데모입니다.
DataContainerSingleton클래스는 실제 싱글이다.
정적 상수 sharedDataContainer를 사용 하여 싱글 톤에 대한 참조를 저장합니다.
싱글 톤에 액세스하려면 구문을 사용하십시오.
DataContainerSingleton.sharedDataContainer
샘플 프로젝트는 데이터 컨테이너에 3 개의 속성을 정의합니다.
var someString: String?
var someOtherString: String?
var someInt: Int?
someInt데이터 컨테이너에서 속성 을로드하려면 다음과 같은 코드를 사용합니다.
let theInt = DataContainerSingleton.sharedDataContainer.someInt
someInt에 값을 저장하려면 다음 구문을 사용합니다.
DataContainerSingleton.sharedDataContainer.someInt = 3
DataContainerSingleton의 init메서드는 UIApplicationDidEnterBackgroundNotification. 해당 코드는 다음과 같습니다.
goToBackgroundObserver = NSNotificationCenter.defaultCenter().addObserverForName(
UIApplicationDidEnterBackgroundNotification,
object: nil,
queue: nil)
{
(note: NSNotification!) -> Void in
let defaults = NSUserDefaults.standardUserDefaults()
//-----------------------------------------------------------------------------
//This code saves the singleton's properties to NSUserDefaults.
//edit this code to save your custom properties
defaults.setObject( self.someString, forKey: DefaultsKeys.someString)
defaults.setObject( self.someOtherString, forKey: DefaultsKeys.someOtherString)
defaults.setObject( self.someInt, forKey: DefaultsKeys.someInt)
//-----------------------------------------------------------------------------
//Tell NSUserDefaults to save to disk now.
defaults.synchronize()
}
관찰자 코드에서 데이터 컨테이너의 속성을 NSUserDefaults. NSCoding상태 데이터를 저장하기 위해, Core Data 또는 기타 다양한 방법을 사용할 수도 있습니다 .
DataContainerSingleton의 init메서드는 또한 속성에 대해 저장된 값을로드하려고합니다.
init 메소드의 해당 부분은 다음과 같습니다.
let defaults = NSUserDefaults.standardUserDefaults()
//-----------------------------------------------------------------------------
//This code reads the singleton's properties from NSUserDefaults.
//edit this code to load your custom properties
someString = defaults.objectForKey(DefaultsKeys.someString) as! String?
someOtherString = defaults.objectForKey(DefaultsKeys.someOtherString) as! String?
someInt = defaults.objectForKey(DefaultsKeys.someInt) as! Int?
//-----------------------------------------------------------------------------
NSUserDefaults에 값을로드하고 저장하기위한 키는 DefaultsKeys다음과 같이 정의 된 struct의 일부인 문자열 상수로 저장됩니다 .
struct DefaultsKeys
{
static let someString = "someString"
static let someOtherString = "someOtherString"
static let someInt = "someInt"
}
다음과 같이 이러한 상수 중 하나를 참조합니다.
DefaultsKeys.someInt
이 샘플 애플리케이션은 데이터 컨테이너 싱글 톤을 세 번 사용합니다.
두 개의 뷰 컨트롤러가 있습니다. 첫 번째는 UIViewController의 사용자 정의 하위 클래스이고 ViewController두 번째는 UIViewController의 사용자 정의 하위 클래스입니다 SecondVC.
두 뷰 컨트롤러에는 모두 텍스트 필드가 있으며 둘 다 데이터 컨테이너 싱글 톤의 someInt속성에서 viewWillAppear메서드 의 텍스트 필드로 값을로드하고 둘 다 텍스트 필드 의 현재 값을 데이터 컨테이너의 'someInt'에 다시 저장합니다.
값을 텍스트 필드에로드하는 코드는 viewWillAppear:메소드에 있습니다.
override func viewWillAppear(animated: Bool)
{
//Load the value "someInt" from our shared ata container singleton
let value = DataContainerSingleton.sharedDataContainer.someInt ?? 0
//Install the value into the text field.
textField.text = "\(value)"
}
사용자가 편집 한 값을 데이터 컨테이너에 다시 저장하는 코드는 뷰 컨트롤러의 textFieldShouldEndEditing메서드에 있습니다.
func textFieldShouldEndEditing(textField: UITextField) -> Bool
{
//Save the changed value back to our data container singleton
DataContainerSingleton.sharedDataContainer.someInt = textField.text!.toInt()
return true
}
뷰 컨트롤러가 표시 될 때마다 UI가 업데이트되도록 viewDidLoad가 아닌 viewWillAppear의 사용자 인터페이스에 값을로드해야합니다.
스위프트 4
신속하게 데이터를 전달하기위한 접근 방식이 너무 많습니다. 여기에 최선의 접근 방식을 추가하고 있습니다.
1) StoryBoard Segue 사용
Storyboard segue는 소스와 대상 뷰 컨트롤러간에 데이터를 전달하는 데 매우 유용하며 그 반대도 마찬가지입니다.
// If you want to pass data from ViewControllerB to ViewControllerA while user tap on back button of ViewControllerB.
@IBAction func unWindSeague (_ sender : UIStoryboardSegue) {
if sender.source is ViewControllerB {
if let _ = sender.source as? ViewControllerB {
self.textLabel.text = "Came from B = B->A , B exited"
}
}
}
// If you want to send data from ViewControllerA to ViewControllerB
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.destination is ViewControllerB {
if let vc = segue.destination as? ViewControllerB {
vc.dataStr = "Comming from A View Controller"
}
}
}
2) 위임 방법 사용
ViewControllerD
//Make the Delegate protocol in Child View Controller (Make the protocol in Class from You want to Send Data)
protocol SendDataFromDelegate {
func sendData(data : String)
}
import UIKit
class ViewControllerD: UIViewController {
@IBOutlet weak var textLabelD: UILabel!
var delegate : SendDataFromDelegate? //Create Delegate Variable for Registering it to pass the data
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
textLabelD.text = "Child View Controller"
}
@IBAction func btnDismissTapped (_ sender : UIButton) {
textLabelD.text = "Data Sent Successfully to View Controller C using Delegate Approach"
self.delegate?.sendData(data:textLabelD.text! )
_ = self.dismiss(animated: true, completion:nil)
}
}
ViewControllerC
import UIKit
class ViewControllerC: UIViewController , SendDataFromDelegate {
@IBOutlet weak var textLabelC: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
@IBAction func btnPushToViewControllerDTapped( _ sender : UIButton) {
if let vcD = self.storyboard?.instantiateViewController(withIdentifier: "ViewControllerD") as? ViewControllerD {
vcD.delegate = self // Registring Delegate (When View Conteoller D gets Dismiss It can call sendData method
// vcD.textLabelD.text = "This is Data Passing by Referenceing View Controller D Text Label." //Data Passing Between View Controllers using Data Passing
self.present(vcD, animated: true, completion: nil)
}
}
//This Method will called when when viewcontrollerD will dismiss. (You can also say it is a implementation of Protocol Method)
func sendData(data: String) {
self.textLabelC.text = data
}
}
ViewControllerA하여 ViewControllerB. 나는 마지막 중괄호 바로 앞에 코드 스 니펫을 내 ViewControllerA.swift( ViewControllerA.swift물론 실제로 파일 이름이 무엇이든간에) 맨 아래에 붙였습니다. " prepare"은 실제로 주어진 클래스에있는 특별한 내장 함수입니다. [아무것도하지 않습니다], 그래서 " override"그것을해야합니다
데이터 컨트롤러 singelton을 만드는 대신 데이터 컨트롤러 인스턴스를 만들어 전달하는 것이 좋습니다. 종속성 주입을 지원하기 위해 먼저 DataController프로토콜을 만듭니다 .
protocol DataController {
var someInt : Int {get set}
var someString : String {get set}
}
그런 다음 SpecificDataController(또는 현재 적절한 이름) 클래스를 만듭니다 .
class SpecificDataController : DataController {
var someInt : Int = 5
var someString : String = "Hello data"
}
ViewController클래스는 다음을 보유 할 필드가 있어야합니다 dataController. 의 유형은 dataController프로토콜 DataController입니다. 이렇게하면 데이터 컨트롤러 구현을 쉽게 전환 할 수 있습니다.
class ViewController : UIViewController {
var dataController : DataController?
...
}
에서 AppDelegate우리는의 ViewController의를 설정할 수 있습니다 dataController:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if let viewController = self.window?.rootViewController as? ViewController {
viewController.dataController = SpecificDataController()
}
return true
}
다른 viewController로 이동할 때 다음을 전달할 수 있습니다 dataController.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
...
}
이제 다른 작업을 위해 데이터 컨트롤러를 전환하려는 경우에서이를 수행 할 수 AppDelegate있으며 데이터 컨트롤러를 사용하는 다른 코드를 변경할 필요가 없습니다.
물론 단순히 단일 값을 전달하려는 경우 이는 과잉입니다. 이 경우 nhgrif의 답변으로 이동하는 것이 가장 좋습니다.
이 접근 방식을 사용하면 논리 부분에서보기를 분리 할 수 있습니다.
@nhgrif가 그의 훌륭한 답변에서 지적했듯이 VC (뷰 컨트롤러)와 다른 개체가 서로 통신 할 수있는 방법에는 여러 가지가 있습니다.
첫 번째 답변에서 설명한 데이터 싱글 톤은 직접 통신하는 것보다 글로벌 상태를 공유하고 저장하는 것에 관한 것입니다.
nhrif의 답변을 사용하면 소스에서 대상 VC로 직접 정보를 보낼 수 있습니다. 회신에서 언급했듯이 대상에서 소스로 메시지를 다시 보낼 수도 있습니다.
실제로 서로 다른 뷰 컨트롤러간에 활성 단방향 또는 양방향 채널을 설정할 수 있습니다. 뷰 컨트롤러가 스토리 보드 segue를 통해 링크 된 경우 링크 설정 시간은 prepareFor Segue 메서드에 있습니다.
부모 뷰 컨트롤러를 사용하여 2 개의 다른 테이블 뷰를 자식으로 호스팅하는 Github에 샘플 프로젝트가 있습니다. 자식 뷰 컨트롤러는 embed segue를 사용하여 연결되고 부모 뷰 컨트롤러는 prepareForSegue 메서드의 각 뷰 컨트롤러와 양방향 링크를 연결합니다.
해당 프로젝트는 github (링크) 에서 찾을 수 있습니다 . 그러나 Objective-C로 작성했지만 Swift로 변환하지 않았으므로 Objective-C에 익숙하지 않은 경우 따라 가기가 조금 어려울 수 있습니다.
SWIFT 3 :
식별 된 segues가있는 스토리 보드가있는 경우 다음을 사용하십시오.
func prepare(for segue: UIStoryboardSegue, sender: Any?)
다른 UIViewController 간의 탐색을 포함하여 프로그래밍 방식으로 모든 작업을 수행하는 경우 메서드를 사용하십시오.
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool)
참고 : UINavigationController를 만드는 데 필요한 두 번째 방법을 사용하려면 UIViewController를 델리게이트로 푸시하고 있으며 UINavigationControllerDelegate 프로토콜을 준수해야합니다.
class MyNavigationController: UINavigationController, UINavigationControllerDelegate {
override func viewDidLoad() {
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
// do what ever you need before going to the next UIViewController or back
//this method will be always called when you are pushing or popping the ViewController
}
}
데이터를 얻고 자하는시기에 따라 다릅니다.
원할 때마다 데이터를 얻으려면 싱글 톤 패턴을 사용할 수 있습니다. 패턴 클래스는 앱 런타임 중에 활성화됩니다. 다음은 싱글 톤 패턴의 예입니다.
class AppSession: NSObject {
static let shared = SessionManager()
var username = "Duncan"
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print(AppSession.shared.username)
}
}
작업 후 데이터를 얻으려면 NotificationCenter를 사용할 수 있습니다.
extension Notification.Name {
static let loggedOut = Notification.Name("loggedOut")
}
@IBAction func logoutAction(_ sender: Any) {
NotificationCenter.default.post(name: .loggedOut, object: nil)
}
NotificationCenter.default.addObserver(forName: .loggedOut, object: nil, queue: OperationQueue.main) { (notify) in
print("User logged out")
}