하루에 0.01%라도 성장하자

Develop/iOS

iOS 오토레이아웃 ( AutoLayout )

뚠님 2019. 5. 14. 18:19
반응형

iOS

 

안드로이드를 하면서 제일 고생했던 것은 레이아웃이다.

레이아웃의 개념을 잡는 것도 어렵긴 했는데... ( 디자인 센스가 1도 없어서.. )

서로 다른 디바이스에서 같은 비율로 UI를 보여준다는 것 자체가 이해가 잘 안됬다... 가능한가 ??? 라는 생각이 먼저였다.

 

안드로이드를 하다보니 weight 개념을 알게되고 어느정도 비율이라는 것을 건드릴 때쯤 iOS에는 AutoLayout이라는 것이 있어서 layout을 자동(?)으로 잡아준다는 이야기를 들었다.

 

처음 안드로이드 앱을 만들때라서 ( 프로젝트 ) 레이아웃으로 피똥(?)싸고 있는데 ㅠㅠ iOS는 그리 쉬운 방법이 있었다니... 

 

AutoLayout

 

뷰의 제약 사항을 바탕으로 뷰 체계 내의 모든뷰의 크기와 위치를 동적으로 계산한다. 앱을 사용할 때 발생하는 외부 변경과 내부 변경에 동적으로 반응하는 사용자 인터페이스를 가능하게 한다.

 

외부 변경 ( External Changes )

 

  • 슈퍼뷰의 크그나 모양이 변경될 때 발생
  • 사용자가 아이패드의 분할뷰(Split View)를 사용하거나 사용하지 않는 경우
  • 장치를 회전하는 경우
  • 활성화콜과 오디오 녹음 바가 보여지거나 사라지는 경우
  • 다른 크기의 클래스를 지원할 경우
  • 다른 크기의 스크린을 지원할 경우
내부 변경 ( Internal Changes )

 

  • 앱 변경에 의해 콘텐츠가 보여지는 경우
  • 국제화를 지원하는 경우
  • 동적 타입을 지원하는 경우
오토 레이아웃이 필요한 이유
  • 앱이 실행되는 iOS 기기의 스크린 화면의 크기가 다양한 경우
  • 스크린이 회전할 수 있는 경우
  • 오디오 바가 보여지거나 사라지는 경우
  • 콘텐츠가 동적으로 보여지는 경우
  • 지역화(Localization)을 지원하는 경우
  • 동적 타입을 지원하는 경우
오토레이아웃 속성

 

Attribute Description
Width 사각형의 너비
Height 사각형의 높이
Top 사각형의 상단
Bottom 사각형의 하단
Baseline 텍스트의 하단
Horizontal 수평
Vertical 수직
Leading 텍스트를 읽을 때 방향
Trailing 텍스트를 읽을 때 끝 방향
CenterX 수평 중심
CenterY 수직 중심

 


 

iOS에는 고유 콘텐츠 크기(Intrinsic Content Size)가 있는데, 이는 뷰가 원래 갖는 크기라고 한다. 즉 레이블의 고유 컨텐츠 크기는 텍스트의 크기고, 이미지의 고유 콘텐츠 크기는 이미지 자체의 크기다.

 

이 개념은 제약 우선도(Constraint Priorities)에서 사용되는데, 크기를 줄이거나 늘리는데도 우선순위가 존재한다.

  1. 콘텐츠 허깅 우선도(Content hugging priority) : 콘텐츠 고유 사이즈보다 뷰가 커지지 않도록 제한. 뷰는 콘텐츠 사이즈 보다 커지지 않는다.
  2. 콘텐츠 축소 방지 우선도(Content compression resistance priority) : 콘텐츠 고유의 사이즈 보다 작아지지 않도록 제한.

 

코드로 구현하기

 

안드로이드와 마찬가지로 UI를 꼭 스토리보드에서 생성하는 것이 아니라 코드로도 생성 할 수 있다.

이 때 우리는 오토레이아웃을 고려해서 작성해야 하는데, 코드로 구현하면 조금은 복잡 할 수 있다.

 

import UIKit
import AVFoundation

class ViewController: UIViewController, AVAudioPlayerDelegate {

    var player : AVAudioPlayer!
    var timer : Timer!
    
    @IBOutlet weak var playBtn: UIButton!
    @IBOutlet weak var playTimeLabel: UILabel!
    @IBOutlet weak var playSilder: UISlider!

    // 재생 버튼을 클릭하면
    @IBAction func touchUpPlayPauseBtn(_ sender : UIButton){
        print("btn change");
        sender.isSelected = !sender.isSelected
        
        // sender가 선택되면 플레이어가 재생중인지 확인하고 처리
        if sender.isSelected {
            self.player?.play()
        } else {
            self.player?.pause()
        }
        
        // sender가 선택되면 타이머 처리를 할 것인지 확인
        if sender.isSelected{
            self.mamkeAndFireTimer()
        } else{
            self.invalidateTimer()
        }
    }
    
    // 플레이어 초기화.
    func initializePlayer(){
        guard let soundAsset : NSDataAsset = NSDataAsset(name : "sound") else {
            print("음원없음");
            return;
        }
        do {
            try self.player = AVAudioPlayer(data : soundAsset.data);
            self.player.delegate = self;
        } catch let error as NSError {
            print("초기화 실패");
            print("코드 : \(error.code), 메세지 : \(error.localizedDescription) ");
        }
        self.playSilder.maximumValue = Float(self.player.duration);
        self.playSilder.minimumValue = 0;
        self.playSilder.value = Float(self.player.currentTime);
    }
    
    // 레이블 업데이트.
    func updateTimeLabelText(time:TimeInterval){
        let minute : Int = Int(time/60);
        let second : Int = Int(time.truncatingRemainder(dividingBy: 60));
        let milisecond : Int = Int(time.truncatingRemainder(dividingBy: 1)*100);
        
        let timeText : String = String(format : "%02ld:%02ld:%02ld", minute, second, milisecond);
        self.playTimeLabel.text = timeText;
    }
    
    // 타이머를 만들고 수행해줄 메소드
    func mamkeAndFireTimer(){
        self.timer = Timer.scheduledTimer(withTimeInterval : 0.01, repeats: true, block : { [unowned self] (timer : Timer) in
            if self.playSilder.isTracking { return };
            self.updateTimeLabelText(time:self.player.currentTime);
            self.playSilder.value = Float(self.player.currentTime);
        })
        self.timer.fire();
    }
    
    
    // 플레이어 재생 관련 에러처리.
    func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
        guard let error: Error = error else{
            print("플레이어 오류 발생");
            return
        }
        
        let message : String
        message = "플레이어 오류 발생 \(error.localizedDescription)";
        
        let alert : UIAlertController = UIAlertController(title:"알림", message : message, preferredStyle :UIAlertController.Style.alert);
        
        let okAction : UIAlertAction = UIAlertAction(title : "확인", style : UIAlertAction.Style.default) { ( action:UIAlertAction) -> Void in
            self.dismiss(animated: true, completion:nil)
        }
        
        alert.addAction(okAction)
        self.present(alert, animated:true, completion:nil)
    }
    
    // 음악 재생이 끝나면
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        self.playBtn.isSelected = false;
        self.playSilder.value = 0;
        self.updateTimeLabelText(time: 0)
        self.invalidateTimer()
    }
    
    // 타이머 해제
    func invalidateTimer(){
        self.timer.invalidate();
        self.timer = nil;
    }
    
    func addViewWithCode(){
        self.addPlayPauseButton();
        self.addTimeLabel();
        self.addProgressSlider();
    }
    
    // 버튼 만들기
    func addPlayPauseButton(){
        
        let button : UIButton = UIButton(type:UIButton.ButtonType.custom);
        button.translatesAutoresizingMaskIntoConstraints = false;
        
        self.view.addSubview(button);
        
        button.setImage(UIImage(named:"button_play"), for: UIControl.State.normal);
        button.setImage(UIImage(named:"button_pause"), for: UIControl.State.selected);
        
        button.addTarget(self, action: #selector(self.touchUpPlayPauseBtn(_:)), for: UIControl.Event.touchUpInside);
        
        let centerX : NSLayoutConstraint
        centerX = button.centerXAnchor.constraint(equalTo:self.view.centerXAnchor);
        
        let centerY : NSLayoutConstraint
        centerY = NSLayoutConstraint(item:button, attribute:NSLayoutConstraint.Attribute.centerY, relatedBy:NSLayoutConstraint.Relation.equal, toItem: self.view, attribute : NSLayoutConstraint.Attribute.centerY, multiplier:0.8, constant:0);
        
        let width:NSLayoutConstraint
        width = button.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier : 0.5 );
        
        let ratio : NSLayoutConstraint
        ratio = button.heightAnchor.constraint(equalTo: button.widthAnchor, multiplier:1)
        
        centerX.isActive = true
        centerY.isActive = true;
        width.isActive = true;
        ratio.isActive = true;
        
        self.playBtn = button;
    }
    
    //타임 라벨 만들기
    func addTimeLabel(){
        let timeLabel : UILabel = UILabel()
        timeLabel.translatesAutoresizingMaskIntoConstraints = false
        
        self.view.addSubview(timeLabel);
        
        timeLabel.textColor = UIColor.black
        timeLabel.textAlignment = NSTextAlignment.center
        timeLabel.font = UIFont.preferredFont(forTextStyle : UIFont.TextStyle.headline);
        
        let centerX: NSLayoutConstraint
        centerX = timeLabel.centerXAnchor.constraint(equalTo: self.playBtn.centerXAnchor);
        
        let top : NSLayoutConstraint
        top = timeLabel.topAnchor.constraint(equalTo : self.playBtn.bottomAnchor, constant:8);
        
        centerX.isActive = true;
        top.isActive = true;
        
        self.playTimeLabel = timeLabel;
        self.updateTimeLabelText(time: 0);
    }
    
    // 슬라이더 만들기
    func addProgressSlider(){
        let slider : UISlider = UISlider();
        
        slider.translatesAutoresizingMaskIntoConstraints = false;
        
        self.view.addSubview(slider);
        
        slider.addTarget(self, action:#selector(self.sliderValueControl(_:)), for:UIControl.Event.valueChanged);
        
        let safeAreaGuide : UILayoutGuide = self.view.safeAreaLayoutGuide
        
        let centerX : NSLayoutConstraint
        centerX = slider.centerXAnchor.constraint(equalTo : self.playTimeLabel.centerXAnchor);
        
        let top: NSLayoutConstraint
        top = slider.topAnchor.constraint(equalTo: self.playTimeLabel.bottomAnchor, constant: 8)
        
        let leading : NSLayoutConstraint
        leading = slider.leadingAnchor.constraint(equalTo: safeAreaGuide.leadingAnchor, constant: 16)
        
        let trailing : NSLayoutConstraint
        trailing = slider.trailingAnchor.constraint(equalTo: safeAreaGuide.trailingAnchor, constant: -16);
        
        centerX.isActive = true;
        top.isActive = true;
        leading.isActive = true;
        trailing.isActive = true;
        
        self.playSilder = slider;
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.addViewWithCode();
        self.initializePlayer();
        // Do any additional setup after loading the view, typically from a nib.
    }

    // 슬라이더를 이용한 오디오 제어
    @IBAction func sliderValueControl(_ sender: UISlider) {
        self.updateTimeLabelText(time: TimeInterval(sender.value))
        if sender.isTracking{return}
        self.player.currentTime = TimeInterval(sender.value);
    }
    
}

 

여기서 핵심은 AutoLayout을 이용하려면 아래 코드가 들어가야 충돌없이 가능하다.

translatesAutoresizingMaskIntoConstraints = false

 

이 코드는 View에 충돌하지 않는 constraint집합을 제공한다고 하는데.. 아 아직은 잘 모르겠다.. 

 

참조할 만한 사이트를 링크한다.. 댓글 보니 많은 도움을 받았다고 하는데.. 나는 아직 이해가 잘 안가서 뭔소린지 ...ㅋㅋㅋ;;;

 

https://zeddios.tistory.com/474

반응형

'Develop > iOS' 카테고리의 다른 글

iOS 음악 재생 앱 만들기 Part#2 - Audio Method  (0) 2019.05.14
iOS 음악 재생 앱 만들기 Part#1 - 앱 개발  (0) 2019.05.14
Swift 오류 처리 ( Error )  (0) 2019.05.13
Swift 익스텐션  (0) 2019.05.13
Swift 프로토콜 (Protocol)  (0) 2019.05.10