web-dev-qa-db-ja.com

自動レイアウトで、画像に応じてImageViewのサイズを動的にするにはどうすればよいですか?

表示する実際の画像のサイズに応じて、UIImageViewを拡大または縮小します。ただし、スーパービューのリーディングエッジから垂直に中央に10ポイント離れたままにしたいのです。

ただし、これらの2つの制約を設定した場合、自動レイアウトを満たすのに十分な制約を設定していないと文句を言います。私には、私が望むものを完全に説明しているようです。私は何が欠けていますか?

46
Doug Smith

画像ビューの固有のサイズはすでに画像のサイズに依存しています。あなたの仮定(そして制約は正しい)。

ただし、インターフェイスビルダーで画像ビューを設定し、画像を提供していない場合、レイアウトシステム(インターフェイスビルダー)は、コンパイル時に画像ビューの大きさを認識しません。画像ビューのサイズはさまざまであるため、レイアウトはあいまいです。これがエラーをスローするものです。

画像ビューの画像プロパティを設定すると、画像ビューの固有のサイズは画像のサイズによって定義されます。実行時にビューを設定している場合は、Annaが述べたことを正確に実行し、イメージビューのプロパティインスペクターで「プレースホルダー」固有のサイズをインターフェイスビルダーに提供できます。これは、インターフェイスビルダーに「今はこのサイズを使用します。後で実際のサイズを提供します」と伝えます。プレースホルダー制約は実行時に無視されます。

もう1つのオプションは、画像をインターフェイスビルダーの画像ビューに直接割り当てることです(ただし、画像は動的であるため、これは機能しません)。

66
John Grant

ここに重要な問題があります:UIImageViewが自動レイアウトと対話する唯一の方法は、そのintrinsicContentSizeプロパティを介することです。このプロパティは、画像自体の固有のサイズを提供しますが、それ以上のものは提供しません。これにより、さまざまなソリューションを使用して3つの状況が作成されます。

ケース1:画像のサイズで表示

最初のケースでは、外部の自動レイアウト制約がその位置のみに影響するコンテキストにUIImageViewを配置すると、ビューのintrinsicContentSizeは、その画像のサイズにしたいことを宣言します。自動レイアウトは、ビューのサイズを画像のサイズに等しくなるように自動的に変更します。これはまさにあなたが望むものであり、それから人生は良いです!

ケース2:画像のアスペクト比を維持した、完全に制限されたビューサイズ

他の場合には、あなたは別のケースにいます。画像ビューに必要なサイズは正確にわかっていますが、表示された画像のアスペクト比を維持することも必要です。この場合、自動レイアウトを使用して画像のサイズを制限しながら、画像ビューで古いcontentModeプロパティを.scaleAspectFitに設定できます。このプロパティは、必要に応じてパディングを追加して、表示された画像を画像ビューで再スケーリングします。そして、これがあなたが望むものであるならば、人生は良いです!

ケース3:画像のアスペクト比に適応した、部分的に制限されたビューサイズ

しかし、多くの場合、トリッキーな3番目のケースです。自動レイアウトを使用して、部分的にビューのサイズを決定し、画像のアスペクト比が他の部分を決定できるようにします。たとえば、自動レイアウト制約を使用してサイズの1次元(垂直タイムラインスクロールの幅など)を決定し、ビューに依存して、画像のアスペクト比に一致する高さが必要であることを自動レイアウトに伝えます(スクロール可能なアイテムが短すぎたり高すぎたりすることはありません)。

イメージビューはintrinsicContentSizeのみを通信するため、これを行うために通常のイメージビューを構成することはできません。そのため、自動レイアウトが1つの寸法を制約するコンテキストに画像ビューを配置する場合(たとえば、幅を短くするように制約する)、画像ビューは、高さをタンデムに再スケーリングすることを自動レイアウトに通知しません。画像自体の高さを変更せずに、固有のコンテンツサイズを報告し続けます。

画像ビューを構成して、自動レイアウトに含まれる画像のアスペクト比を取得したいことを伝えるには、その効果に新しい制約を追加する必要があります。さらに、イメージ自体が更新されるたびに、その制約を更新する必要があります。これを行うUIImageViewサブクラスを構築できるため、動作は自動です。以下に例を示します。

public class ScaleAspectFitImageView : UIImageView {
  /// constraint to maintain same aspect ratio as the image
  private var aspectRatioConstraint:NSLayoutConstraint? = nil

  required public init?(coder aDecoder: NSCoder) {
    super.init(coder:aDecoder)
    self.setup()
  }

  public override init(frame:CGRect) {
    super.init(frame:frame)
    self.setup()
  }

  public override init(image: UIImage!) {
    super.init(image:image)
    self.setup()
  }

  public override init(image: UIImage!, highlightedImage: UIImage?) {
    super.init(image:image,highlightedImage:highlightedImage)
    self.setup()
  }

  override public var image: UIImage? {
    didSet {
      self.updateAspectRatioConstraint()
    }
  }

  private func setup() {
    self.contentMode = .scaleAspectFit
    self.updateAspectRatioConstraint()
  }

  /// Removes any pre-existing aspect ratio constraint, and adds a new one based on the current image
  private func updateAspectRatioConstraint() {
    // remove any existing aspect ratio constraint
    if let c = self.aspectRatioConstraint {
      self.removeConstraint(c)
    }
    self.aspectRatioConstraint = nil

    if let imageSize = image?.size, imageSize.height != 0
    {
      let aspectRatio = imageSize.width / imageSize.height
      let c = NSLayoutConstraint(item: self, attribute: .width,
                                 relatedBy: .equal,
                                 toItem: self, attribute: .height,
                                 multiplier: aspectRatio, constant: 0)
      // a priority above fitting size level and below low
      c.priority = (UILayoutPriorityDefaultLow + UILayoutPriorityFittingSizeLevel) / 2.0
      self.addConstraint(c)
      self.aspectRatioConstraint = c
    }
  }
}

より多くのコメントは this Gist で利用可能です

36
algal

@algal case 3の場合、私がしなければならなかったことは

class ScaledHeightImageView: UIImageView {

    override var intrinsicContentSize: CGSize {

        if let myImage = self.image {
            let myImageWidth = myImage.size.width
            let myImageHeight = myImage.size.height
            let myViewWidth = self.frame.size.width

            let ratio = myViewWidth/myImageWidth
            let scaledHeight = myImageHeight * ratio

            return CGSize(width: myViewWidth, height: scaledHeight)
        }
        return CGSize(width: -1.0, height: -1.0)
    }
}

これにより、intrinsicContentSizeの高さコンポーネントがスケーリングされます。これはスーパークラスのデフォルトの動作であるため、画像がない場合は(-1.0、-1.0)が返されます。

また、画像ビューを含むセルを含むテーブルにUITableViewAutomaticDimensionを設定する必要はなく、セルのサイズは自動的に変わりません。画像ビューのコンテンツモードはAspect Fit

10
xirix

これは、1つのディメンションのみが制約されている場合に自動レイアウトで機能するAspectFitUIImageViewから派生した実装です。もう1つは自動的に設定されます。画像のアスペクト比を維持し、画像の周囲に余白を追加しません。

@ algal idea のObjective-C実装を調整しましたが、次の違いがあります。

  • _150_と評価される式priority = (UILayoutPriorityDefaultLow + UILayoutPriorityFittingSizeLevel) / 2.0は、デフォルトの画像コンテンツサイズの制約の_1000_の優先度に勝るには不十分です。そのため、アスペクト制約の優先度も_1000_に引き上げられました。
  • 必要な場合にのみ、アスペクト比の制約を削除または再追加します。これは重い操作なので、アスペクト比が同じであればそうする必要はありません。さらに、image setterは複数回呼び出される可能性があるため、ここで余分なレイアウト計算を行わないことをお勧めします。
  • なぜrequired public init?(coder aDecoder: NSCoder)public override init(frame:CGRect)をオーバーライドする必要があるのか​​わからないため、これら2つは削除されます。それらはUIImageViewによってオーバーライドされません。そのため、画像が設定されるまでアスペクト制約を追加する意味がありません。

_AspectKeepUIImageView.h_:

_NS_ASSUME_NONNULL_BEGIN

@interface AspectKeepUIImageView : UIImageView

- (instancetype)initWithImage:(nullable UIImage *)image;
- (instancetype)initWithImage:(nullable UIImage *)image highlightedImage:(nullable UIImage *)highlightedImage;

@end

NS_ASSUME_NONNULL_END
_

_AspectKeepUIImageView.m_:

_#import "AspectKeepUIImageView.h"

@implementation AspectKeepUIImageView
{
    NSLayoutConstraint *_aspectContraint;
}

- (instancetype)initWithImage:(nullable UIImage *)image
{
    self = [super initWithImage:image];

    [self initInternal];

    return self;
}

- (instancetype)initWithImage:(nullable UIImage *)image highlightedImage:(nullable UIImage *)highlightedImage
{
    self = [super initWithImage:image highlightedImage:highlightedImage];

    [self initInternal];

    return self;
}

- (void)initInternal
{
    self.contentMode = UIViewContentModeScaleAspectFit;
    [self updateAspectConstraint];
}

- (void)setImage:(UIImage *)image
{
    [super setImage:image];
    [self updateAspectConstraint];
}

- (void)updateAspectConstraint
{
    CGSize imageSize = self.image.size;
    CGFloat aspectRatio = imageSize.height > 0.0f
        ? imageSize.width / imageSize.height
        : 0.0f;
    if (_aspectContraint.multiplier != aspectRatio)
    {
        [self removeConstraint:_aspectContraint];

        _aspectContraint =
        [NSLayoutConstraint constraintWithItem:self
                                     attribute:NSLayoutAttributeWidth
                                     relatedBy:NSLayoutRelationEqual
                                        toItem:self
                                     attribute:NSLayoutAttributeHeight
                                    multiplier:aspectRatio
                                      constant:0.f];

        _aspectContraint.priority = UILayoutPriorityRequired;

        [self addConstraint:_aspectContraint];
    }
}

@end
_
2